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) 2012
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.mppsscp;
040
041import java.io.File;
042import java.io.IOException;
043import java.security.GeneralSecurityException;
044import java.util.Properties;
045import java.util.ResourceBundle;
046import java.util.concurrent.ExecutorService;
047import java.util.concurrent.Executors;
048import java.util.concurrent.ScheduledExecutorService;
049
050import org.apache.commons.cli.CommandLine;
051import org.apache.commons.cli.OptionBuilder;
052import org.apache.commons.cli.Options;
053import org.apache.commons.cli.ParseException;
054import org.dcm4che3.data.Tag;
055import org.dcm4che3.data.UID;
056import org.dcm4che3.data.Attributes;
057import org.dcm4che3.data.IOD;
058import org.dcm4che3.data.ValidationResult;
059import org.dcm4che3.io.DicomInputStream;
060import org.dcm4che3.io.DicomOutputStream;
061import org.dcm4che3.net.ApplicationEntity;
062import org.dcm4che3.net.Association;
063import org.dcm4che3.net.Connection;
064import org.dcm4che3.net.Device;
065import org.dcm4che3.net.Status;
066import org.dcm4che3.net.TransferCapability;
067import org.dcm4che3.net.service.BasicCEchoSCP;
068import org.dcm4che3.net.service.BasicMPPSSCP;
069import org.dcm4che3.net.service.DicomServiceException;
070import org.dcm4che3.net.service.DicomServiceRegistry;
071import org.dcm4che3.tool.common.CLIUtils;
072import org.dcm4che3.util.SafeClose;
073import org.slf4j.Logger;
074import org.slf4j.LoggerFactory;
075
076/**
077 * @author Gunter Zeilinger <gunterze@gmail.com>
078 */
079public class MppsSCP {
080
081    private static ResourceBundle rb =
082            ResourceBundle.getBundle("org.dcm4che3.tool.mppsscp.messages");
083
084    private static final Logger LOG = LoggerFactory.getLogger(MppsSCP.class);
085
086    private Device device = new Device("mppsscp");
087    private final ApplicationEntity ae = new ApplicationEntity("*");
088    private final Connection conn = new Connection();
089    private File storageDir;
090    private IOD mppsNCreateIOD;
091    private IOD mppsNSetIOD;
092
093    protected final BasicMPPSSCP mppsSCP = new BasicMPPSSCP() {
094
095        @Override
096        protected Attributes create(Association as, Attributes rq,
097                                    Attributes rqAttrs, Attributes rsp) throws DicomServiceException {
098            return MppsSCP.this.create(as, rq, rqAttrs);
099        }
100
101        @Override
102        protected Attributes set(Association as, Attributes rq, Attributes rqAttrs,
103                                 Attributes rsp) throws DicomServiceException {
104            return MppsSCP.this.set(as, rq, rqAttrs);
105        }
106    };
107    private boolean started = false;
108
109    public MppsSCP() throws IOException {
110        device.addConnection(conn);
111        device.addApplicationEntity(ae);
112        ae.setAssociationAcceptor(true);
113        ae.addConnection(conn);
114        DicomServiceRegistry serviceRegistry = new DicomServiceRegistry();
115        serviceRegistry.addDicomService(new BasicCEchoSCP());
116        serviceRegistry.addDicomService(mppsSCP);
117        ae.setDimseRQHandler(serviceRegistry);
118    }
119
120    /**
121     * Bind the MPPS SCP to the provided preconfigured ae
122     *
123     * @param applicationEntity
124     */
125    public MppsSCP(ApplicationEntity applicationEntity) {
126        device = applicationEntity.getDevice();
127        applicationEntity.setAssociationAcceptor(true);
128        DicomServiceRegistry serviceRegistry = new DicomServiceRegistry();
129        serviceRegistry.addDicomService(new BasicCEchoSCP());
130        serviceRegistry.addDicomService(mppsSCP);
131        applicationEntity.setDimseRQHandler(serviceRegistry);
132    }
133
134    private void configure(boolean hasOptionNoValidate,
135                           String mppsNCreateIOD,
136                           String mppsNSetIOD,
137                           boolean ignore,
138                           String directory) throws IOException, GeneralSecurityException {
139        configureStorageDirectory(this, ignore, directory);
140        configureIODs(this, hasOptionNoValidate,
141                mppsNCreateIOD,
142                mppsNSetIOD);
143
144        ExecutorService executorService = Executors.newCachedThreadPool();
145        ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
146        device.setScheduledExecutor(scheduledExecutorService);
147        device.setExecutor(executorService);
148    }
149
150    public void start() throws IOException, GeneralSecurityException {
151        device.bindConnections();
152        started = true;
153    }
154
155    public void stop() {
156
157        if (!started) return;
158
159        started = false;
160
161        device.unbindConnections();
162        ((ExecutorService) device.getExecutor()).shutdown();
163        device.getScheduledExecutor().shutdown();
164
165        //very quick fix to block for listening connection
166        while (device.getConnections().get(0).isListening())
167        {
168            try {
169                Thread.sleep(10);
170            } catch (InterruptedException e) {
171                // ignore
172            }
173        }
174
175    }
176
177    public void setStorageDirectory(File storageDir) {
178        if (storageDir != null)
179            storageDir.mkdirs();
180        this.storageDir = storageDir;
181    }
182
183    public File getStorageDirectory() {
184        return storageDir;
185    }
186
187    private void setMppsNCreateIOD(IOD mppsNCreateIOD) {
188        this.mppsNCreateIOD = mppsNCreateIOD;
189    }
190
191    private void setMppsNSetIOD(IOD mppsNSetIOD) {
192        this.mppsNSetIOD = mppsNSetIOD;
193    }
194
195    public static void main(String[] args) {
196        try {
197            CommandLine cl = parseComandLine(args);
198            MppsSCP main = new MppsSCP();
199
200            CLIUtils.configureBindServer(main.conn, main.ae, cl);
201            CLIUtils.configure(main.conn, cl);
202            configureTransferCapability(main.ae, cl);
203
204            main.configure(cl.hasOption("no-validate"),
205                    cl.getOptionValue("mpps-ncreate-iod", "resource:mpps-ncreate-iod.xml"),
206                    cl.getOptionValue("mpps-nset-iod", "resource:mpps-nset-iod.xml"),
207                    cl.hasOption("ignore"),
208                    cl.getOptionValue("directory", "."));
209
210            main.start();
211
212        } catch (ParseException e) {
213            System.err.println("mppsscp: " + e.getMessage());
214            System.err.println(rb.getString("try"));
215            System.exit(2);
216        } catch (Exception e) {
217            System.err.println("mppsscp: " + e.getMessage());
218            e.printStackTrace();
219            System.exit(2);
220        }
221    }
222
223    private static CommandLine parseComandLine(String[] args) throws ParseException {
224        Options opts = new Options();
225        CLIUtils.addBindServerOption(opts);
226        CLIUtils.addAEOptions(opts);
227        CLIUtils.addCommonOptions(opts);
228        addStorageDirectoryOptions(opts);
229        addTransferCapabilityOptions(opts);
230        addIODOptions(opts);
231        return CLIUtils.parseComandLine(args, opts, rb, MppsSCP.class);
232    }
233
234    @SuppressWarnings("static-access")
235    private static void addStorageDirectoryOptions(Options opts) {
236        opts.addOption(null, "ignore", false,
237                rb.getString("ignore"));
238        opts.addOption(OptionBuilder
239                .hasArg()
240                .withArgName("path")
241                .withDescription(rb.getString("directory"))
242                .withLongOpt("directory")
243                .create(null));
244    }
245
246    @SuppressWarnings("static-access")
247    private static void addTransferCapabilityOptions(Options opts) {
248        opts.addOption(OptionBuilder
249                .hasArg()
250                .withArgName("file|url")
251                .withDescription(rb.getString("sop-classes"))
252                .withLongOpt("sop-classes")
253                .create(null));
254    }
255
256    @SuppressWarnings("static-access")
257    private static void addIODOptions(Options opts) {
258        opts.addOption(null, "no-validate", false,
259                rb.getString("no-validate"));
260        opts.addOption(OptionBuilder
261                .hasArg()
262                .withArgName("file|url")
263                .withDescription(rb.getString("ncreate-iod"))
264                .withLongOpt("ncreate-iod")
265                .create(null));
266        opts.addOption(OptionBuilder
267                .hasArg()
268                .withArgName("file|url")
269                .withDescription(rb.getString("nset-iod"))
270                .withLongOpt("nset-iod")
271                .create(null));
272    }
273
274    private static void configureStorageDirectory(MppsSCP main, boolean hasOptionIgnore, String directory) {
275        if (!hasOptionIgnore) {
276            main.setStorageDirectory(new File(directory));
277        }
278    }
279
280    private static void configureIODs(MppsSCP main, boolean hasOptionNoValidate, String mppsNCreateIOD, String mppsNSetIOD)
281            throws IOException {
282        if (!hasOptionNoValidate) {
283            main.setMppsNCreateIOD(IOD.load(mppsNCreateIOD));
284            main.setMppsNSetIOD(IOD.load(mppsNSetIOD));
285        }
286    }
287
288    private static void configureTransferCapability(ApplicationEntity ae,
289                                                    CommandLine cl) throws IOException {
290        Properties p = CLIUtils.loadProperties(
291                cl.getOptionValue("sop-classes",
292                        "resource:sop-classes.properties"),
293                null);
294        for (String cuid : p.stringPropertyNames()) {
295            String ts = p.getProperty(cuid);
296            ae.addTransferCapability(
297                    new TransferCapability(null,
298                            CLIUtils.toUID(cuid),
299                            TransferCapability.Role.SCP,
300                            CLIUtils.toUIDs(ts)));
301        }
302    }
303
304    private Attributes create(Association as, Attributes rq, Attributes rqAttrs)
305            throws DicomServiceException {
306        if (mppsNCreateIOD != null) {
307            ValidationResult result = rqAttrs.validate(mppsNCreateIOD);
308            if (!result.isValid())
309                throw DicomServiceException.valueOf(result, rqAttrs);
310        }
311        if (storageDir == null)
312            return null;
313        String cuid = rq.getString(Tag.AffectedSOPClassUID);
314        String iuid = rq.getString(Tag.AffectedSOPInstanceUID);
315        File file = new File(storageDir, iuid);
316        if (file.exists())
317            throw new DicomServiceException(Status.DuplicateSOPinstance).
318                    setUID(Tag.AffectedSOPInstanceUID, iuid);
319        DicomOutputStream out = null;
320        LOG.info("{}: M-WRITE {}", as, file);
321        try {
322            out = new DicomOutputStream(file);
323            out.writeDataset(
324                    Attributes.createFileMetaInformation(iuid, cuid, UID.ExplicitVRLittleEndian), rqAttrs);
325        } catch (IOException e) {
326            LOG.warn(as + ": Failed to store MPPS:", e);
327            throw new DicomServiceException(Status.ProcessingFailure, e);
328        } finally {
329            SafeClose.close(out);
330        }
331        return null;
332    }
333
334    private Attributes set(Association as, Attributes rq, Attributes rqAttrs)
335            throws DicomServiceException {
336        if (mppsNSetIOD != null) {
337            ValidationResult result = rqAttrs.validate(mppsNSetIOD);
338            if (!result.isValid())
339                throw DicomServiceException.valueOf(result, rqAttrs);
340        }
341        if (storageDir == null)
342            return null;
343        String cuid = rq.getString(Tag.RequestedSOPClassUID);
344        String iuid = rq.getString(Tag.RequestedSOPInstanceUID);
345        File file = new File(storageDir, iuid);
346        if (!file.exists())
347            throw new DicomServiceException(Status.NoSuchObjectInstance).
348                    setUID(Tag.AffectedSOPInstanceUID, iuid);
349        LOG.info("{}: M-UPDATE {}", as, file);
350        Attributes data;
351        DicomInputStream in = null;
352        try {
353            in = new DicomInputStream(file);
354            data = in.readDataset(-1, -1);
355        } catch (IOException e) {
356            LOG.warn(as + ": Failed to read MPPS:", e);
357            throw new DicomServiceException(Status.ProcessingFailure, e);
358        } finally {
359            SafeClose.close(in);
360        }
361        if (!"IN PROGRESS".equals(data.getString(Tag.PerformedProcedureStepStatus)))
362            BasicMPPSSCP.mayNoLongerBeUpdated();
363
364        data.addAll(rqAttrs);
365        DicomOutputStream out = null;
366        try {
367            out = new DicomOutputStream(file);
368            out.writeDataset(
369                    Attributes.createFileMetaInformation(iuid, cuid, UID.ExplicitVRLittleEndian),
370                    data);
371        } catch (IOException e) {
372            LOG.warn(as + ": Failed to update MPPS:", e);
373            throw new DicomServiceException(Status.ProcessingFailure, e);
374        } finally {
375            SafeClose.close(out);
376        }
377        return null;
378    }
379}