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.net;
040
041import java.io.IOException;
042import java.io.InputStream;
043import java.io.OutputStream;
044import java.net.Socket;
045import java.util.Collections;
046import java.util.HashMap;
047import java.util.Set;
048import java.util.concurrent.TimeUnit;
049import java.util.concurrent.atomic.AtomicInteger;
050
051import org.dcm4che3.data.Attributes;
052import org.dcm4che3.data.Tag;
053import org.dcm4che3.data.UID;
054import org.dcm4che3.data.VR;
055import org.dcm4che3.net.pdu.AAbort;
056import org.dcm4che3.net.pdu.AAssociateAC;
057import org.dcm4che3.net.pdu.AAssociateRJ;
058import org.dcm4che3.net.pdu.AAssociateRQ;
059import org.dcm4che3.net.pdu.CommonExtendedNegotiation;
060import org.dcm4che3.net.pdu.PresentationContext;
061import org.dcm4che3.net.pdu.RoleSelection;
062import org.dcm4che3.util.IntHashMap;
063import org.dcm4che3.util.SafeClose;
064import org.slf4j.Logger;
065import org.slf4j.LoggerFactory;
066
067/**
068 * @author Gunter Zeilinger <gunterze@gmail.com>
069 */
070public class Association {
071
072    public static final Logger LOG = LoggerFactory.getLogger(Association.class);
073
074    private static final AtomicInteger prevSerialNo = new AtomicInteger();
075    private final AtomicInteger messageID = new AtomicInteger();
076    private final int serialNo;
077    private final boolean requestor;
078    private String name;
079    private ApplicationEntity ae;
080    private final Device device;
081    private final Connection conn;
082    private final Socket sock;
083    private final InputStream in;
084    private final OutputStream out;
085    private final PDUEncoder encoder;
086    private PDUDecoder decoder;
087    private State state;
088    private AAssociateRQ rq;
089    private AAssociateAC ac;
090    private IOException ex;
091
092    private HashMap<String, Object> properties;
093    private int maxOpsInvoked;
094    private int maxPDULength;
095    private int performing;
096    private Timeout timeout;
097    private final IntHashMap<DimseRSPHandler> rspHandlerForMsgId =
098            new IntHashMap<DimseRSPHandler>();
099    private final IntHashMap<CancelRQHandler> cancelHandlerForMsgId =
100            new IntHashMap<CancelRQHandler>();
101    private final HashMap<String,HashMap<String,PresentationContext>> pcMap =
102            new HashMap<String,HashMap<String,PresentationContext>>();
103
104    Association(ApplicationEntity ae, Connection local, Socket sock)
105            throws IOException {
106        this.serialNo = prevSerialNo.incrementAndGet();
107        this.ae = ae;
108        this.requestor = ae != null;
109        this.name = "" + sock.getLocalSocketAddress()
110             + delim() + sock.getRemoteSocketAddress()
111             + '(' + serialNo + ')';
112        this.conn = local;
113        this.device = local.getDevice();
114        this.sock = sock;
115        this.in = sock.getInputStream();
116        this.out = sock.getOutputStream();
117        this.encoder = new PDUEncoder(this, out);
118        if (requestor) {
119            enterState(State.Sta4);
120        } else {
121            enterState(State.Sta2);
122            startRequestTimeout();
123        }
124        activate();
125    }
126
127    public Device getDevice() {
128         return device;
129    }
130
131    public int nextMessageID() {
132        return messageID.incrementAndGet() & 0xFFFF;
133    }
134
135    private String delim() {
136        return requestor ? "->" : "<-";
137    }
138
139    @Override
140    public String toString() {
141        return name;
142    }
143
144    public final Socket getSocket() {
145        return sock;
146    }
147
148    public final Connection getConnection() {
149        return conn;
150    }
151
152    public final AAssociateRQ getAAssociateRQ() {
153        return rq;
154    }
155
156    public final AAssociateAC getAAssociateAC() {
157        return ac;
158    }
159
160    public final IOException getException() {
161        return ex;
162    }
163
164    public final ApplicationEntity getApplicationEntity() {
165        return ae;
166    }
167
168    public Object getProperty(String key) {
169        return properties != null ? properties.get(key) : null;
170    }
171
172    @SuppressWarnings("unchecked")
173    public <T> T getProperty(Class<T> clazz) {
174        return (T) getProperty(clazz.getName());
175    }
176
177    public void setProperty(Object value) {
178        setProperty(value.getClass().getName(), value);
179    }
180
181    public boolean containsProperty(String key) {
182        return properties != null && properties.containsKey(key);
183    }
184
185    public Object setProperty(String key, Object value) {
186        if (properties == null)
187            properties = new HashMap<String, Object>();
188        return properties.put(key, value);
189    }
190
191    public Object clearProperty(String key) {
192        return properties != null ? properties.remove(key) : null;
193    }
194
195    public final boolean isRequestor() {
196        return requestor;
197    }
198
199    public boolean isReadyForDataTransfer() {
200        return state == State.Sta6;
201    }
202
203    private void checkIsSCP(String cuid) throws NoRoleSelectionException {
204        if (!isSCPFor(cuid))
205            throw new NoRoleSelectionException(cuid,
206                    TransferCapability.Role.SCP);
207    }
208
209    public boolean isSCPFor(String cuid) {
210        RoleSelection rolsel = ac.getRoleSelectionFor(cuid);
211        if (rolsel == null)
212            return !requestor;
213        return requestor ? rolsel.isSCP() : rolsel.isSCU();
214    }
215
216    private void checkIsSCU(String cuid) throws NoRoleSelectionException {
217        if (!isSCUFor(cuid))
218            throw new NoRoleSelectionException(cuid,
219                    TransferCapability.Role.SCU);
220    }
221
222    public boolean isSCUFor(String cuid) {
223        RoleSelection rolsel = ac.getRoleSelectionFor(cuid);
224        if (rolsel == null)
225            return requestor;
226        return requestor ? rolsel.isSCU() : rolsel.isSCP();
227    }
228
229    public String getCallingAET() {
230        return rq != null ? rq.getCallingAET() : null;
231    }
232
233    public String getCalledAET() {
234        return rq != null ? rq.getCalledAET() : null;
235    }
236
237    public String getRemoteAET() {
238        return requestor ? getCalledAET() : getCallingAET();
239    }
240
241    public String getLocalAET() {
242        return requestor ? getCallingAET() : getCalledAET();
243    }
244
245    public String getRemoteImplVersionName() {
246        return (requestor ? ac : rq).getImplVersionName();
247    }
248
249    public String getRemoteImplClassUID() {
250        return (requestor ? ac : rq).getImplClassUID();
251    }
252
253    public String getLocalImplVersionName() {
254        return (requestor ? rq : ac).getImplVersionName();
255    }
256
257    public String getLocalImplClassUID() {
258        return (requestor ? rq : ac).getImplClassUID();
259    }
260
261    final int getMaxPDULengthSend() {
262        return maxPDULength;
263    }
264
265    boolean isPackPDV() {
266        return conn.isPackPDV();
267    }
268
269    public void release() throws IOException {
270        state.writeAReleaseRQ(this);
271    }
272
273    public void abort() {
274        abort(new AAbort());
275    }
276
277    void abort(AAbort aa) {
278        try {
279            state.write(this, aa);
280        } catch (IOException e) {
281            // already handled by onIOException()
282            // do not bother user about
283        }
284    }
285
286    private synchronized void closeSocket() {
287        state.closeSocket(this);
288    }
289
290    void doCloseSocket() {
291        LOG.info("{}: close {}", name, sock);
292        SafeClose.close(sock);
293        enterState(State.Sta1);
294    }
295
296    synchronized private void closeSocketDelayed() {
297        state.closeSocketDelayed(this);
298    }
299
300    void doCloseSocketDelayed() {
301        enterState(State.Sta13);
302        int delay = conn.getSocketCloseDelay();
303        if (delay > 0)
304            device.schedule(new Runnable() {
305
306                @Override
307                public void run() {
308                    closeSocket();
309                }
310            }, delay, TimeUnit.MILLISECONDS);
311        else
312            closeSocket();
313    }
314
315    synchronized void onIOException(IOException e) {
316        if (ex != null)
317            return;
318
319        ex = e;
320        LOG.warn("{}: i/o exception: {} in State: {}",
321                new Object[] { name, e, state });
322        closeSocket();
323    }
324
325    void write(AAbort aa) throws IOException  {
326        LOG.info("{} << {}", name, aa);
327        encoder.write(aa);
328        ex = aa;
329        closeSocketDelayed();
330    }
331
332    void writeAReleaseRQ() throws IOException {
333        LOG.info("{} << A-RELEASE-RQ", name);
334        enterState(State.Sta7);
335        stopTimeout();
336        encoder.writeAReleaseRQ();
337        startReleaseTimeout();
338    }
339
340    private void startRequestTimeout() {
341        startTimeout("{}: start A-ASSOCIATE-RQ timeout of {}ms",
342                "{}: A-ASSOCIATE-RQ timeout expired",
343                "{}: stop A-ASSOCIATE-RQ timeout",
344                conn.getRequestTimeout(), State.Sta2);
345    }
346
347    private void startAcceptTimeout() {
348        startTimeout("{}: start A-ASSOCIATE-AC timeout of {}ms",
349                "{}: A-ASSOCIATE-AC timeout expired",
350                "{}: stop A-ASSOCIATE-AC timeout",
351                conn.getAcceptTimeout(), State.Sta5);
352    }
353
354    private void startReleaseTimeout() {
355        startTimeout("{}: start A-RELEASE-RP timeout of {}ms",
356                "{}: A-RELEASE-RP timeout expired",
357                "{}: stop A-RELEASE-RP timeout",
358                conn.getReleaseTimeout(), State.Sta7);
359    }
360
361    private void startIdleTimeout() {
362        startTimeout("{}: start idle timeout of {}ms",
363                "{}: idle timeout expired",
364                "{}: stop idle timeout",
365                conn.getIdleTimeout(), State.Sta6);
366    }
367
368    private void startTimeout(String startMsg, String expiredMsg,
369            String cancelMsg, int timeout, State state) {
370        if (timeout > 0 && performing == 0 && rspHandlerForMsgId.isEmpty()) {
371            synchronized (this) {
372                if (this.state == state) {
373                    stopTimeout();
374                    this.timeout = Timeout.start(this, startMsg, expiredMsg,
375                            cancelMsg, timeout);
376                }
377            }
378        }
379    }
380
381    private void startTimeout(final int msgID, int timeout) {
382        if (timeout > 0) {
383            synchronized (rspHandlerForMsgId) {
384                DimseRSPHandler rspHandler = rspHandlerForMsgId.get(msgID);
385                if (rspHandler != null) {
386                    rspHandler.setTimeout(Timeout.start(this,
387                        "{}: start " + msgID + ":DIMSE-RSP timeout of {}ms",
388                        "{}: " + msgID + ":DIMSE-RSP timeout expired",
389                        "{}: stop " + msgID + ":DIMSE-RSP timeout",
390                        timeout));
391                }
392            }
393        }
394    }
395
396    private synchronized void stopTimeout() {
397        if (timeout != null) {
398            timeout.stop();
399            timeout = null;
400        }
401    }
402
403    public void waitForOutstandingRSP() throws InterruptedException {
404        synchronized (rspHandlerForMsgId) {
405            while (!rspHandlerForMsgId.isEmpty())
406                rspHandlerForMsgId.wait();
407        }
408    }
409
410    void write(AAssociateRQ rq) throws IOException {
411        name = rq.getCallingAET() + delim() + rq.getCalledAET() + '(' + serialNo + ')';
412        this.rq = rq;
413        LOG.info("{} << A-ASSOCIATE-RQ", name);
414        LOG.debug("{}", rq);
415        enterState(State.Sta5);
416        encoder.write(rq);
417        startAcceptTimeout();
418    }
419
420    private void write(AAssociateAC ac) throws IOException {
421        LOG.info("{} << A-ASSOCIATE-AC", name);
422        LOG.debug("{}", ac);
423        enterState(State.Sta6);
424        encoder.write(ac);
425        startIdleTimeout();
426    }
427
428    private void write(AAssociateRJ e) throws IOException {
429        LOG.info("{} << {}", name, e);
430        encoder.write(e);
431        closeSocketDelayed();
432    }
433
434    private void checkException() throws IOException {
435        if (ex != null)
436            throw ex;
437    }
438
439    private synchronized void enterState(State newState) {
440        LOG.debug("{}: enter state: {}", name, newState);
441        this.state = newState;
442        notifyAll();
443    }
444
445    public final State getState() {
446        return state;
447    }
448
449    synchronized void waitForLeaving(State state)
450            throws InterruptedException, IOException {
451        while (this.state == state)
452            wait();
453        checkException();
454    }
455
456    synchronized void waitForEntering(State state)
457            throws InterruptedException, IOException {
458        while (this.state != state)
459            wait();
460        checkException();
461    }
462
463    public void waitForSocketClose()
464            throws InterruptedException, IOException {
465        waitForEntering(State.Sta1);
466    }
467
468    private void activate() {
469        device.execute(new Runnable() {
470
471            @Override
472            public void run() {
473                decoder = new PDUDecoder(Association.this, in);
474                device.incrementNumberOfOpenAssociations();
475                try {
476                    try {
477                        while (!(state == State.Sta1 || state == State.Sta13))
478                            decoder.nextPDU();
479                    } catch (AAbort aa) {
480                        abort(aa);
481                    } catch (IOException e) {
482                        onIOException(e);
483                    } finally {
484                        onClose();
485                    }
486                } finally {
487                    device.decrementNumberOfOpenAssociations();
488                }
489            }
490        });
491    }
492
493    private void onClose() {
494        stopTimeout();
495        synchronized (rspHandlerForMsgId) {
496            IntHashMap.Visitor<DimseRSPHandler> visitor =
497                    new IntHashMap.Visitor<DimseRSPHandler>() {
498
499                @Override
500                public boolean visit(int key, DimseRSPHandler value) {
501                    value.onClose(Association.this);
502                    return true;
503                }
504            };
505            rspHandlerForMsgId.accept(visitor);
506            rspHandlerForMsgId.clear();
507            rspHandlerForMsgId.notifyAll();
508        }
509        if (ae != null)
510            ae.getDevice().getAssociationHandler().onClose(this);
511    }
512
513    void onAAssociateRQ(AAssociateRQ rq) throws IOException {
514        name = rq.getCalledAET() + delim() + rq.getCallingAET() + '(' + serialNo + ')';
515        LOG.info("{} >> A-ASSOCIATE-RQ", name);
516        LOG.debug("{}", rq);
517        stopTimeout();
518        state.onAAssociateRQ(this, rq);
519    }
520
521    void handle(AAssociateRQ rq) throws IOException {
522        this.rq = rq;
523        enterState(State.Sta3);
524        try {
525            ae = device.getApplicationEntity(rq.getCalledAET());
526            ac = device.getAssociationHandler().negotiate(this, rq);
527            initPCMap();
528            maxOpsInvoked = ac.getMaxOpsPerformed();
529            maxPDULength = Association.minZeroAsMax(
530                    rq.getMaxPDULength(), conn.getSendPDULength());
531            write(ac);
532        } catch (AAssociateRJ e) {
533            write(e);
534        }
535    }
536
537    void onAAssociateAC(AAssociateAC ac) throws IOException {
538        LOG.info("{} >> A-ASSOCIATE-AC", name);
539        LOG.debug("{}", ac);
540        stopTimeout();
541        state.onAAssociateAC(this, ac);
542    }
543
544    void handle(AAssociateAC ac) throws IOException {
545        this.ac = ac;
546        initPCMap();
547        maxOpsInvoked = ac.getMaxOpsInvoked();
548        maxPDULength = Association.minZeroAsMax(
549                ac.getMaxPDULength(), conn.getSendPDULength());
550        enterState(State.Sta6);
551        startIdleTimeout();
552    }
553
554    void onAAssociateRJ(AAssociateRJ rj) throws IOException {
555        LOG.info("{} >> {}", name, rj);
556        state.onAAssociateRJ(this, rj);
557    }
558
559    void handle(AAssociateRJ rq) {
560        ex = rq;
561        closeSocket();
562    }
563
564    void onAReleaseRQ() throws IOException {
565        LOG.info("{} >> A-RELEASE-RQ", name);
566        stopTimeout();
567        state.onAReleaseRQ(this);
568    }
569
570    void handleAReleaseRQ() throws IOException {
571        enterState(State.Sta8);
572        waitForPerformingOps();
573        LOG.info("{} << A-RELEASE-RP", name);
574        encoder.writeAReleaseRP();
575        closeSocketDelayed();
576    }
577
578    private synchronized void waitForPerformingOps() {
579        while (performing > 0 && state == State.Sta8) {
580            try {
581                wait();
582            } catch (InterruptedException e) {
583                Thread.currentThread().interrupt();
584            }
585        }
586    }
587
588    void handleAReleaseRQCollision() throws IOException {
589        if (isRequestor()) {
590            enterState(State.Sta9);
591            LOG.info("{} << A-RELEASE-RP", name);
592            encoder.writeAReleaseRP();
593            enterState(State.Sta11);
594        } else {
595            enterState(State.Sta10);
596        }
597    }
598
599    void onAReleaseRP() throws IOException {
600        LOG.info("{} >> A-RELEASE-RP", name);
601        stopTimeout();
602        state.onAReleaseRP(this);
603    }
604
605    void handleAReleaseRP() throws IOException {
606        closeSocket();
607    }
608
609   void handleAReleaseRPCollision() throws IOException {
610        enterState(State.Sta12);
611        LOG.info("{} << A-RELEASE-RP", name);
612        encoder.writeAReleaseRP();
613        closeSocketDelayed();
614   }
615
616    void onAAbort(AAbort aa) {
617        LOG.info("{} >> {}", name, aa);
618        stopTimeout();
619        ex = aa;
620        closeSocket();
621    }
622
623    void unexpectedPDU(String pdu) throws AAbort {
624        LOG.warn("{} >> unexpected {} in state: {}",
625                new Object[] { name, pdu, state });
626        throw new AAbort(AAbort.UL_SERIVE_PROVIDER, AAbort.UNEXPECTED_PDU);
627    }
628
629    void onPDataTF() throws IOException {
630        state.onPDataTF(this);
631    }
632
633    void handlePDataTF() throws IOException {
634        decoder.decodeDIMSE();
635    }
636
637    void writePDataTF() throws IOException {
638        checkException();
639        state.writePDataTF(this);
640    }
641
642    void doWritePDataTF() throws IOException {
643        encoder.writePDataTF();
644    }
645
646    void onDimseRQ(PresentationContext pc, Dimse dimse, Attributes cmd,
647            PDVInputStream data) throws IOException {
648        stopTimeout();
649        incPerforming();
650        ae.onDimseRQ(this, pc, dimse, cmd, data);
651    }
652
653    private synchronized void incPerforming() {
654        ++performing;
655    }
656
657    private synchronized void decPerforming() {
658        --performing;
659        notifyAll();
660    }
661
662    void onDimseRSP(Dimse dimse, Attributes cmd, Attributes data) throws AAbort {
663        int msgId = cmd.getInt(Tag.MessageIDBeingRespondedTo, -1);
664        int status = cmd.getInt(Tag.Status, 0);
665        boolean pending = Status.isPending(status);
666        DimseRSPHandler rspHandler = getDimseRSPHandler(msgId);
667        if (rspHandler == null) {
668            Dimse.LOG.info("{}: unexpected message ID in DIMSE RSP:", name);
669            Dimse.LOG.info("\n{}", cmd);
670            throw new AAbort();
671        }
672        rspHandler.onDimseRSP(this, cmd, data);
673        if (pending)
674            startTimeout(msgId, dimse.isRetrieveRQ()
675                    ? conn.getRetrieveTimeout()
676                    : conn.getResponseTimeout());
677        else {
678            removeDimseRSPHandler(msgId);
679            if (rspHandlerForMsgId.isEmpty() && performing == 0)
680                startIdleOrReleaseTimeout();
681        }
682    }
683
684    private synchronized void startIdleOrReleaseTimeout() {
685        if (state == State.Sta6)
686            startIdleTimeout();
687        else if (state == State.Sta7)
688            startReleaseTimeout();
689    }
690
691    private void addDimseRSPHandler(DimseRSPHandler rspHandler)
692            throws InterruptedException {
693        synchronized (rspHandlerForMsgId) {
694            while (maxOpsInvoked > 0
695                    && rspHandlerForMsgId.size() >= maxOpsInvoked)
696                rspHandlerForMsgId.wait();
697            rspHandlerForMsgId.put(rspHandler.getMessageID(), rspHandler);
698        }
699    }
700
701    private DimseRSPHandler getDimseRSPHandler(int msgId) {
702        synchronized (rspHandlerForMsgId ) {
703            return rspHandlerForMsgId.get(msgId);
704        }
705    }
706
707    private DimseRSPHandler removeDimseRSPHandler(int msgId) {
708        synchronized (rspHandlerForMsgId ) {
709            DimseRSPHandler tmp = rspHandlerForMsgId.remove(msgId);
710            rspHandlerForMsgId.notifyAll();
711            return tmp;
712        }
713    }
714
715    void cancel(PresentationContext pc, int msgId) throws IOException {
716        Attributes cmd = Commands.mkCCancelRQ(msgId);
717        encoder.writeDIMSE(pc, cmd, null);
718    }
719
720    public boolean tryWriteDimseRSP(PresentationContext pc, Attributes cmd) {
721        return tryWriteDimseRSP(pc, cmd, null);
722    }
723
724    public boolean tryWriteDimseRSP(PresentationContext pc, Attributes cmd,
725            Attributes data) {
726        try {
727            writeDimseRSP(pc, cmd, data);
728            return true;
729        } catch (IOException e) {
730            LOG.warn("{} << {} failed: {}", new Object[] {
731                        this,
732                        Dimse.valueOf(cmd.getInt(Tag.CommandField, 0)),
733                        e.getMessage() });
734            return false;
735        }
736    }
737
738    public void writeDimseRSP(PresentationContext pc, Attributes cmd)
739            throws IOException {
740        writeDimseRSP(pc, cmd, null);
741    }
742
743    public void writeDimseRSP(PresentationContext pc, Attributes cmd,
744            Attributes data) throws IOException {
745        DataWriter writer = null;
746        int datasetType = Commands.NO_DATASET;
747        if (data != null) {
748            writer = new DataWriterAdapter(data);
749            datasetType = Commands.getWithDatasetType();
750        }
751        cmd.setInt(Tag.CommandDataSetType, VR.US, datasetType);
752        encoder.writeDIMSE(pc, cmd, writer);
753        if (!Status.isPending(cmd.getInt(Tag.Status, 0))) {
754            decPerforming();
755            startIdleTimeout();
756        }
757    }
758
759    void onCancelRQ(Attributes cmd) throws IOException {
760        int msgId = cmd.getInt(Tag.MessageIDBeingRespondedTo, -1);
761        CancelRQHandler handler = removeCancelRQHandler(msgId);
762        if (handler != null)
763            handler.onCancelRQ(this);
764    }
765
766    public void addCancelRQHandler(int msgId, CancelRQHandler handler) {
767        synchronized (cancelHandlerForMsgId) {
768            cancelHandlerForMsgId.put(msgId, handler);
769        }
770    }
771
772    public CancelRQHandler removeCancelRQHandler(int msgId) {
773        synchronized (cancelHandlerForMsgId) {
774            return cancelHandlerForMsgId.remove(msgId);
775        }
776    }
777
778    private void initPCMap() {
779        for (PresentationContext pc : ac.getPresentationContexts())
780            if (pc.isAccepted())
781                initTSMap(rq.getPresentationContext(pc.getPCID())
782                            .getAbstractSyntax())
783                        .put(pc.getTransferSyntax(), pc);
784    }
785
786    private HashMap<String, PresentationContext> initTSMap(String as) {
787        HashMap<String, PresentationContext> tsMap = pcMap.get(as);
788        if (tsMap == null)
789            pcMap.put(as, tsMap = new HashMap<String, PresentationContext>());
790        return tsMap;
791    }
792
793    private PresentationContext pcFor(String cuid, String tsuid)
794            throws NoPresentationContextException {
795        HashMap<String, PresentationContext> tsMap = pcMap.get(cuid);
796        if (tsMap == null)
797            throw new NoPresentationContextException(cuid);
798        if (tsuid == null)
799            return tsMap.values().iterator().next();
800        PresentationContext pc = tsMap.get(tsuid);
801        if (pc == null)
802            throw new NoPresentationContextException(cuid, tsuid);
803        return pc;
804    }
805
806    public Set<String> getTransferSyntaxesFor(String cuid) {
807        HashMap<String, PresentationContext> tsMap = pcMap.get(cuid);
808        if (tsMap == null)
809            return Collections.emptySet();
810        return Collections.unmodifiableSet(tsMap.keySet());
811    }
812
813    PresentationContext getPresentationContext(int pcid) {
814        return ac.getPresentationContext(pcid);
815    }
816
817    public CommonExtendedNegotiation getCommonExtendedNegotiationFor(
818            String cuid) {
819        return ac.getCommonExtendedNegotiationFor(cuid);
820    }
821
822    public void cstore(String cuid, String iuid, int priority, DataWriter data,
823            String tsuid, DimseRSPHandler rspHandler) throws IOException,
824            InterruptedException {
825        cstore(cuid, cuid, iuid, priority, data, tsuid, rspHandler);
826    }
827
828    public void cstore(String asuid, String cuid, String iuid, int priority,
829            DataWriter data, String tsuid, DimseRSPHandler rspHandler)
830            throws IOException, InterruptedException {
831        PresentationContext pc = pcFor(asuid, tsuid);
832        checkIsSCU(cuid);
833        Attributes cstorerq = Commands.mkCStoreRQ(rspHandler.getMessageID(),
834                cuid, iuid, priority);
835        invoke(pc, cstorerq, data, rspHandler, conn.getResponseTimeout());
836    }
837
838    public DimseRSP cstore(String cuid, String iuid, int priority,
839            DataWriter data, String tsuid)
840            throws IOException, InterruptedException {
841        return cstore(cuid, cuid, iuid, priority, data, tsuid);
842    }
843
844    public DimseRSP cstore(String asuid, String cuid, String iuid,
845            int priority, DataWriter data, String tsuid)
846            throws IOException, InterruptedException {
847        FutureDimseRSP rsp = new FutureDimseRSP(nextMessageID());
848        cstore(asuid, cuid, iuid, priority, data, tsuid, rsp);
849        return rsp;
850    }
851
852    public void cstore(String cuid, String iuid, int priority,
853            String moveOriginatorAET, int moveOriginatorMsgId,
854            DataWriter data, String tsuid, DimseRSPHandler rspHandler)
855            throws IOException, InterruptedException {
856        cstore(cuid, cuid, iuid, priority, moveOriginatorAET,
857                moveOriginatorMsgId, data, tsuid, rspHandler);
858    }
859
860    public void cstore(String asuid, String cuid, String iuid, int priority,
861            String moveOriginatorAET, int moveOriginatorMsgId,
862            DataWriter data, String tsuid, DimseRSPHandler rspHandler)
863            throws IOException, InterruptedException {
864        PresentationContext pc = pcFor(asuid, tsuid);
865        Attributes cstorerq = Commands.mkCStoreRQ(rspHandler.getMessageID(),
866                cuid, iuid, priority, moveOriginatorAET, moveOriginatorMsgId);
867        invoke(pc, cstorerq, data, rspHandler, conn.getResponseTimeout());
868    }
869
870    public DimseRSP cstore(String cuid, String iuid, int priority,
871            String moveOriginatorAET, int moveOriginatorMsgId,
872            DataWriter data, String tsuid) throws IOException,
873            InterruptedException {
874        return cstore(cuid, cuid, iuid, priority, moveOriginatorAET,
875                moveOriginatorMsgId, data, tsuid);
876    }
877
878    public DimseRSP cstore(String asuid, String cuid, String iuid,
879            int priority, String moveOriginatorAET, int moveOriginatorMsgId,
880            DataWriter data, String tsuid) throws IOException,
881            InterruptedException {
882        FutureDimseRSP rsp = new FutureDimseRSP(nextMessageID());
883        cstore(asuid, cuid, iuid, priority, moveOriginatorAET,
884                moveOriginatorMsgId, data, tsuid, rsp);
885        return rsp;
886    }
887
888    public void cfind(String cuid, int priority, Attributes data,
889            String tsuid, DimseRSPHandler rspHandler) throws IOException,
890            InterruptedException {
891        cfind(cuid, cuid, priority, data, tsuid, rspHandler);
892    }
893
894    public void cfind(String asuid, String cuid, int priority,
895            Attributes data, String tsuid, DimseRSPHandler rspHandler)
896            throws IOException, InterruptedException {
897        PresentationContext pc = pcFor(asuid, tsuid);
898        checkIsSCU(cuid);
899        Attributes cfindrq =
900                Commands.mkCFindRQ(rspHandler.getMessageID(), cuid, priority);
901        invoke(pc, cfindrq, new DataWriterAdapter(data), rspHandler,
902                conn.getResponseTimeout());
903    }
904
905    public DimseRSP cfind(String cuid, int priority, Attributes data,
906            String tsuid, int autoCancel) throws IOException,
907            InterruptedException {
908        return cfind(cuid, cuid, priority, data, tsuid, autoCancel);
909    }
910
911    public DimseRSP cfind(String asuid, String cuid, int priority,
912            Attributes data, String tsuid, int autoCancel) throws IOException,
913            InterruptedException {
914        FutureDimseRSP rsp = new FutureDimseRSP(nextMessageID());
915        rsp.setAutoCancel(autoCancel);
916        cfind(asuid, cuid, priority, data, tsuid, rsp);
917        return rsp;
918    }
919
920    public void cget(String cuid, int priority, Attributes data,
921            String tsuid, DimseRSPHandler rspHandler)
922            throws IOException, InterruptedException {
923        cget(cuid, cuid, priority, data, tsuid, rspHandler);
924    }
925
926    public void cget(String asuid, String cuid, int priority,
927            Attributes data, String tsuid, DimseRSPHandler rspHandler)
928            throws IOException,
929            InterruptedException {
930        PresentationContext pc = pcFor(asuid, tsuid);
931        checkIsSCU(cuid);
932        Attributes cgetrq = Commands.mkCGetRQ(rspHandler.getMessageID(),
933                cuid, priority);
934        invoke(pc, cgetrq, new DataWriterAdapter(data), rspHandler,
935                conn.getRetrieveTimeout());
936    }
937
938    public DimseRSP cget(String cuid, int priority, Attributes data,
939            String tsuid) throws IOException,
940            InterruptedException {
941        return cget(cuid, cuid, priority, data, tsuid);
942    }
943
944    public DimseRSP cget(String asuid, String cuid, int priority,
945            Attributes data, String tsuid)
946            throws IOException, InterruptedException {
947        FutureDimseRSP rsp = new FutureDimseRSP(nextMessageID());
948        cget(asuid, cuid, priority, data, tsuid, rsp);
949        return rsp;
950    }
951
952    public void cmove(String cuid, int priority, Attributes data,
953            String tsuid, String destination, DimseRSPHandler rspHandler)
954            throws IOException, InterruptedException {
955        cmove(cuid, cuid, priority, data, tsuid, destination, rspHandler);
956    }
957
958    public void cmove(String asuid, String cuid, int priority,
959            Attributes data, String tsuid, String destination,
960            DimseRSPHandler rspHandler) throws IOException,
961            InterruptedException {
962        PresentationContext pc = pcFor(asuid, tsuid);
963        checkIsSCU(cuid);
964        Attributes cmoverq = Commands.mkCMoveRQ(rspHandler.getMessageID(),
965                cuid, priority, destination);
966        invoke(pc, cmoverq, new DataWriterAdapter(data), rspHandler,
967                conn.getRetrieveTimeout());
968    }
969
970    public DimseRSP cmove(String cuid, int priority, Attributes data,
971            String tsuid, String destination) throws IOException,
972            InterruptedException {
973        return cmove(cuid, cuid, priority, data, tsuid, destination);
974    }
975
976    public DimseRSP cmove(String asuid, String cuid, int priority,
977            Attributes data, String tsuid, String destination)
978            throws IOException, InterruptedException {
979        FutureDimseRSP rsp = new FutureDimseRSP(nextMessageID());
980        cmove(asuid, cuid, priority, data, tsuid, destination, rsp);
981        return rsp;
982    }
983
984    public DimseRSP cecho() throws IOException, InterruptedException {
985        return cecho(UID.VerificationSOPClass);
986    }
987
988    public DimseRSP cecho(String cuid) throws IOException, InterruptedException {
989        FutureDimseRSP rsp = new FutureDimseRSP(nextMessageID());
990        PresentationContext pc = pcFor(cuid, null);
991        checkIsSCU(cuid);
992        Attributes cechorq = Commands.mkCEchoRQ(rsp.getMessageID(), cuid);
993        invoke(pc, cechorq, null, rsp, conn.getResponseTimeout());
994        return rsp;
995    }
996
997    public void neventReport(String cuid, String iuid, int eventTypeId,
998            Attributes data, String tsuid, DimseRSPHandler rspHandler)
999            throws IOException, InterruptedException {
1000        neventReport(cuid, cuid, iuid, eventTypeId, data, tsuid, rspHandler);
1001    }
1002
1003    public void neventReport(String asuid, String cuid, String iuid, int eventTypeId,
1004            Attributes data, String tsuid, DimseRSPHandler rspHandler)
1005            throws IOException, InterruptedException {
1006        PresentationContext pc = pcFor(asuid, tsuid);
1007        checkIsSCP(cuid);
1008        Attributes neventrq =
1009                Commands.mkNEventReportRQ(rspHandler.getMessageID(), cuid, iuid,
1010                        eventTypeId, data);
1011        invoke(pc, neventrq, DataWriterAdapter.forAttributes(data), rspHandler,
1012                conn.getResponseTimeout());
1013    }
1014
1015    public DimseRSP neventReport(String cuid, String iuid, int eventTypeId,
1016            Attributes data, String tsuid) throws IOException,
1017            InterruptedException {
1018        return neventReport(cuid, cuid, iuid, eventTypeId, data, tsuid);
1019    }
1020
1021    public DimseRSP neventReport(String asuid, String cuid, String iuid,
1022            int eventTypeId, Attributes data, String tsuid)
1023            throws IOException, InterruptedException {
1024        FutureDimseRSP rsp = new FutureDimseRSP(nextMessageID());
1025        neventReport(asuid, cuid, iuid, eventTypeId, data, tsuid, rsp);
1026        return rsp;
1027    }
1028
1029    public void nget(String cuid, String iuid, int[] tags,
1030            DimseRSPHandler rspHandler)
1031            throws IOException, InterruptedException {
1032        nget(cuid, cuid, iuid, tags, rspHandler);
1033    }
1034
1035    public void nget(String asuid, String cuid, String iuid, int[] tags,
1036            DimseRSPHandler rspHandler)
1037            throws IOException, InterruptedException {
1038        PresentationContext pc = pcFor(asuid, null);
1039        checkIsSCU(cuid);
1040        Attributes ngetrq =
1041                Commands.mkNGetRQ(rspHandler.getMessageID(), cuid, iuid, tags);
1042        invoke(pc, ngetrq, null, rspHandler, conn.getResponseTimeout());
1043    }
1044
1045    public DimseRSP nget(String cuid, String iuid, int[] tags)
1046            throws IOException, InterruptedException {
1047        return nget(cuid, cuid, iuid, tags);
1048    }
1049
1050    public DimseRSP nget(String asuid, String cuid, String iuid, int[] tags)
1051            throws IOException, InterruptedException {
1052        FutureDimseRSP rsp = new FutureDimseRSP(nextMessageID());
1053        nget(asuid, cuid, iuid, tags, rsp);
1054        return rsp;
1055    }
1056
1057    public void nset(String cuid, String iuid, Attributes data,
1058            String tsuid, DimseRSPHandler rspHandler)
1059            throws IOException, InterruptedException {
1060        nset(cuid, cuid, iuid, new DataWriterAdapter(data), tsuid, rspHandler);
1061    }
1062
1063    public void nset(String asuid, String cuid, String iuid,
1064            Attributes data, String tsuid, DimseRSPHandler rspHandler)
1065            throws IOException, InterruptedException {
1066        nset(asuid, cuid, iuid, new DataWriterAdapter(data), tsuid, rspHandler);
1067    }
1068
1069    public DimseRSP nset(String cuid, String iuid, Attributes data,
1070            String tsuid) throws IOException,
1071            InterruptedException {
1072        return nset(cuid, cuid, iuid, new DataWriterAdapter(data), tsuid);
1073    }
1074
1075    public DimseRSP nset(String asuid, String cuid, String iuid,
1076            Attributes data, String tsuid)
1077            throws IOException, InterruptedException {
1078        return nset(asuid, cuid, iuid, new DataWriterAdapter(data), tsuid);
1079    }
1080
1081    public void nset(String cuid, String iuid, DataWriter data,
1082            String tsuid, DimseRSPHandler rspHandler)
1083            throws IOException, InterruptedException {
1084        nset(cuid, cuid, iuid, data, tsuid, rspHandler);
1085    }
1086
1087    public void nset(String asuid, String cuid, String iuid,
1088            DataWriter data, String tsuid, DimseRSPHandler rspHandler)
1089            throws IOException, InterruptedException {
1090        PresentationContext pc = pcFor(asuid, tsuid);
1091        checkIsSCU(cuid);
1092        Attributes nsetrq =
1093                Commands.mkNSetRQ(rspHandler.getMessageID(), cuid, iuid);
1094        invoke(pc, nsetrq, data, rspHandler, conn.getResponseTimeout());
1095    }
1096
1097    public DimseRSP nset(String cuid, String iuid, DataWriter data,
1098            String tsuid) throws IOException,
1099            InterruptedException {
1100        return nset(cuid, cuid, iuid, data, tsuid);
1101    }
1102
1103    public DimseRSP nset(String asuid, String cuid, String iuid,
1104            DataWriter data, String tsuid)
1105            throws IOException, InterruptedException {
1106        FutureDimseRSP rsp = new FutureDimseRSP(nextMessageID());
1107        nset(asuid, cuid, iuid, data, tsuid, rsp);
1108        return rsp;
1109    }
1110
1111    public void naction(String cuid, String iuid, int actionTypeId,
1112            Attributes data, String tsuid, DimseRSPHandler rspHandler)
1113            throws IOException, InterruptedException {
1114        naction(cuid, cuid, iuid, actionTypeId, data, tsuid, rspHandler);
1115    }
1116
1117    public void naction(String asuid, String cuid, String iuid, int actionTypeId,
1118            Attributes data, String tsuid, DimseRSPHandler rspHandler)
1119            throws IOException, InterruptedException {
1120        PresentationContext pc = pcFor(asuid, tsuid);
1121        checkIsSCU(cuid);
1122        Attributes nactionrq =
1123                Commands.mkNActionRQ(rspHandler.getMessageID(), cuid, iuid,
1124                        actionTypeId, data);
1125        invoke(pc, nactionrq, DataWriterAdapter.forAttributes(data), rspHandler,
1126                conn.getResponseTimeout());
1127    }
1128
1129    public DimseRSP naction(String cuid, String iuid, int actionTypeId,
1130            Attributes data, String tsuid) throws IOException,
1131            InterruptedException {
1132        return naction(cuid, cuid, iuid, actionTypeId, data, tsuid);
1133    }
1134
1135    public DimseRSP naction(String asuid, String cuid, String iuid,
1136            int actionTypeId, Attributes data, String tsuid)
1137            throws IOException, InterruptedException {
1138        FutureDimseRSP rsp = new FutureDimseRSP(nextMessageID());
1139        naction(asuid, cuid, iuid, actionTypeId, data, tsuid, rsp);
1140        return rsp;
1141    }
1142
1143    public void ncreate(String cuid, String iuid,  Attributes data,
1144            String tsuid, DimseRSPHandler rspHandler)
1145            throws IOException, InterruptedException {
1146        ncreate(cuid, cuid, iuid, data, tsuid, rspHandler);
1147    }
1148
1149    public void ncreate(String asuid, String cuid, String iuid,
1150            Attributes data, String tsuid, DimseRSPHandler rspHandler)
1151            throws IOException, InterruptedException {
1152        PresentationContext pc = pcFor(asuid, tsuid);
1153        checkIsSCU(cuid);
1154        Attributes ncreaterq =
1155                Commands.mkNCreateRQ(rspHandler.getMessageID(), cuid, iuid);
1156        invoke(pc, ncreaterq, DataWriterAdapter.forAttributes(data), rspHandler,
1157                conn.getResponseTimeout());
1158    }
1159
1160    public DimseRSP ncreate(String cuid, String iuid, Attributes data,
1161            String tsuid) throws IOException,
1162            InterruptedException {
1163        return ncreate(cuid, cuid, iuid, data, tsuid);
1164    }
1165
1166    public DimseRSP ncreate(String asuid, String cuid, String iuid,
1167            Attributes data, String tsuid)
1168            throws IOException, InterruptedException {
1169        FutureDimseRSP rsp = new FutureDimseRSP(nextMessageID());
1170        ncreate(asuid, cuid, iuid, data, tsuid, rsp);
1171        return rsp;
1172    }
1173
1174    public void ndelete(String cuid, String iuid, DimseRSPHandler rspHandler)
1175            throws IOException, InterruptedException {
1176        ndelete(cuid, cuid, iuid, rspHandler);
1177    }
1178
1179    public void ndelete(String asuid, String cuid, String iuid,
1180            DimseRSPHandler rspHandler)
1181            throws IOException, InterruptedException {
1182        PresentationContext pc = pcFor(asuid, null);
1183        checkIsSCU(cuid);
1184        Attributes ndeleterq =
1185                Commands.mkNDeleteRQ(rspHandler.getMessageID(), cuid, iuid);
1186        invoke(pc, ndeleterq, null, rspHandler, conn.getResponseTimeout());
1187    }
1188
1189    public DimseRSP ndelete(String cuid, String iuid)
1190            throws IOException, InterruptedException {
1191        return ndelete(cuid, cuid, iuid);
1192    }
1193
1194    public DimseRSP ndelete(String asuid, String cuid, String iuid)
1195            throws IOException, InterruptedException {
1196        FutureDimseRSP rsp = new FutureDimseRSP(nextMessageID());
1197        ndelete(asuid, cuid, iuid, rsp);
1198        return rsp;
1199    }
1200
1201    private void invoke(PresentationContext pc, Attributes cmd,
1202            DataWriter data, DimseRSPHandler rspHandler, int rspTimeout)
1203            throws IOException, InterruptedException {
1204        stopTimeout();
1205        checkException();
1206        rspHandler.setPC(pc);
1207        addDimseRSPHandler(rspHandler);
1208        startTimeout(rspHandler.getMessageID(), rspTimeout);
1209        encoder.writeDIMSE(pc, cmd, data);
1210    }
1211
1212    static int minZeroAsMax(int i1, int i2) {
1213        return i1 == 0 ? i2 : i2 == 0 ? i1 : Math.min(i1, i2);
1214    }
1215
1216    public Attributes createFileMetaInformation(String iuid, String cuid,
1217            String tsuid) {
1218        Attributes fmi = new Attributes(7);
1219        fmi.setBytes(Tag.FileMetaInformationVersion, VR.OB, new byte[] { 0, 1 });
1220        fmi.setString(Tag.MediaStorageSOPClassUID, VR.UI, cuid);
1221        fmi.setString(Tag.MediaStorageSOPInstanceUID, VR.UI, iuid);
1222        fmi.setString(Tag.TransferSyntaxUID, VR.UI, tsuid);
1223        fmi.setString(Tag.ImplementationClassUID, VR.UI,
1224                getRemoteImplClassUID());
1225        String versionName = getRemoteImplVersionName();
1226        if (versionName != null)
1227            fmi.setString(Tag.ImplementationVersionName, VR.SH, versionName);
1228        fmi.setString(Tag.SourceApplicationEntityTitle, VR.SH,
1229                getRemoteAET());
1230        return fmi;
1231    }
1232
1233    /**
1234     * Releases the association in a graceful way.
1235     * <ul>
1236     *     <li>makes sure the current state is eligible for calling a release() before doing so</li>
1237     *     <li>waits for outstanding RSP before release</li>
1238     *     <li>handles exceptions and logs detected issues</li>
1239     * </ul>
1240     *
1241     * This method will not throw exceptions normally.
1242     */
1243    public void releaseGracefully() {
1244        if (isReadyForDataTransfer()) {
1245
1246            try {
1247                waitForOutstandingRSP();
1248            } catch (InterruptedException ignored) {
1249                // if we get interrupted while trying to close the association, most likely we still want to close it
1250                Thread.currentThread().interrupt();
1251                LOG.warn("Interrupted while preparing to close the association, will try to release the association anyway: " + this.toString(), ignored);
1252            }
1253
1254            try {
1255                release();
1256            } catch (IOException e) {
1257                LOG.warn("Failed to release association to " + getRemoteAET(), e);
1258            }
1259
1260        } else {
1261            LOG.warn("Attempted to close the association, but it was not ready for data transfer", new IOException("Association not ready for data transfer"));
1262        }
1263    }
1264}
1265