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 org.dcm4che3.conf.core.api.ConfigurableClass;
042import org.dcm4che3.conf.core.api.ConfigurableProperty;
043import org.dcm4che3.conf.core.api.ConfigurableProperty.ConfigurablePropertyType;
044import org.dcm4che3.conf.core.api.ConfigurableProperty.Tag;
045import org.dcm4che3.conf.core.api.LDAP;
046import org.dcm4che3.conf.core.api.Parent;
047import org.dcm4che3.data.Attributes;
048import org.dcm4che3.net.pdu.*;
049import org.dcm4che3.util.StringUtils;
050import org.slf4j.Logger;
051import org.slf4j.LoggerFactory;
052
053import java.io.IOException;
054import java.io.Serializable;
055import java.net.Socket;
056import java.security.GeneralSecurityException;
057import java.util.*;
058
059/**
060 * DICOM Part 15, Annex H compliant description of a DICOM network service.
061 * <p/>
062 * A Network AE is an application entity that provides services on a network. A
063 * Network AE will have the 16 same functional capability regardless of the
064 * particular network connection used. If there are functional differences based
065 * on selected network connection, then these are separate Network AEs. If there
066 * are 18 functional differences based on other internal structures, then these
067 * are separate Network AEs.
068 *
069 * @author Gunter Zeilinger <gunterze@gmail.com>
070 */
071@LDAP(objectClasses = {"dcmNetworkAE", "dicomNetworkAE"}, distinguishingField = "dicomAETitle")
072@ConfigurableClass(referable = true)
073public class ApplicationEntity implements Serializable {
074
075    private static final long serialVersionUID = 3883790997057469573L;
076
077    protected static final Logger LOG = LoggerFactory.getLogger(ApplicationEntity.class);
078
079
080    @ConfigurableProperty(name = "dicomAETitle", tags = Tag.PRIMARY)
081    private String AETitle;
082
083    @ConfigurableProperty(type = ConfigurablePropertyType.UUID, description = "An immutable unique identifier")
084    private String uuid = UUID.randomUUID().toString();
085
086    @ConfigurableProperty(name = "dicomDescription")
087    private String description;
088
089    @ConfigurableProperty(type = ConfigurablePropertyType.OptimisticLockingHash)
090    private String olockHash;
091
092    @ConfigurableProperty(name = "dicomVendorData")
093    private byte[][] vendorData = {};
094
095    @ConfigurableProperty(name = "dicomApplicationCluster")
096    private String[] applicationClusters = {};
097
098    @ConfigurableProperty(name = "dicomPreferredCalledAETitle")
099    private String[] preferredCalledAETitles = {};
100
101    @ConfigurableProperty(name = "dicomPreferredCallingAETitle")
102    private String[] preferredCallingAETitles = {};
103
104    @ConfigurableProperty(name = "dicomSupportedCharacterSet")
105    private String[] supportedCharacterSets = {};
106
107    @ConfigurableProperty(name = "dicomInstalled")
108    private Boolean aeInstalled;
109
110    @ConfigurableProperty(name = "dcmAcceptedCallingAETitle")
111    private final Set<String> acceptedCallingAETitlesSet =
112            new LinkedHashSet<String>();
113
114    // Connections are dereferenced by DicomConfiguration
115    @ConfigurableProperty(name = "dicomNetworkConnectionReference", collectionOfReferences = true, tags = Tag.PRIMARY)
116    private final List<Connection> connections = new ArrayList<Connection>(1);
117
118    /**
119     * "Proxy" property, actually forwards everything to scuTCs and scpTCs in its setter/getter
120     */
121    @LDAP(noContainerNode = true)
122    @ConfigurableProperty(name = "dcmTransferCapability",
123            description = "DICOM Transfer Capabilities",
124            tags = Tag.PRIMARY)
125    private Collection<TransferCapability> transferCapabilities;
126
127    // populated/collected by transferCapabilities' setter/getter
128    private final Map<String, TransferCapability> scuTCs =
129            new TreeMap<String, TransferCapability>();
130
131    // populated/collected by transferCapabilities' setter/getter
132    private final Map<String, TransferCapability> scpTCs =
133            new TreeMap<String, TransferCapability>();
134
135    @ConfigurableProperty(name = "aeExtensions", isExtensionsProperty = true)
136    private Map<Class<? extends AEExtension>, AEExtension> extensions =
137            new HashMap<Class<? extends AEExtension>, AEExtension>();
138
139    @ConfigurableProperty(name = "dicomAssociationAcceptor")
140    private boolean associationAcceptor = true;
141
142    @ConfigurableProperty(name = "dicomAssociationInitiator")
143    private boolean associationInitiator = true;
144
145    @ConfigurableProperty(name = "dcmAETitleAliases",
146            label = "Aliases (alternative AE titles)")
147    private List<String> AETitleAliases = new ArrayList<String>();
148
149    @Parent
150    private Device device;
151
152    private transient DimseRQHandler dimseRQHandler;
153
154    public ApplicationEntity() {
155    }
156
157    public ApplicationEntity(String aeTitle) {
158        setAETitle(aeTitle);
159    }
160
161    public List<String> getAETitleAliases() {
162        return new ArrayList<String>(AETitleAliases);
163    }
164
165    public void setAETitleAliases(List<String> AETitleAliases) {
166        this.AETitleAliases = AETitleAliases;
167    }
168
169    public Map<Class<? extends AEExtension>, AEExtension> getExtensions() {
170        return extensions;
171    }
172
173    public void setExtensions(Map<Class<? extends AEExtension>, AEExtension> extensions) {
174        this.extensions = extensions;
175    }
176
177    public String getUuid() {
178        return uuid;
179    }
180
181    public void setUuid(String uuid) {
182        this.uuid = uuid;
183    }
184
185    public void setTransferCapabilities(Collection<TransferCapability> transferCapabilities) {
186        scpTCs.clear();
187        scuTCs.clear();
188
189        for (TransferCapability tc : transferCapabilities) {
190            tc.setApplicationEntity(this);
191            switch (tc.getRole()) {
192                case SCP:
193                    scpTCs.put(tc.getSopClass(), tc);
194                    break;
195                case SCU:
196                    scuTCs.put(tc.getSopClass(), tc);
197            }
198        }
199    }
200
201    public Collection<TransferCapability> getTransferCapabilities() {
202        ArrayList<TransferCapability> tcs =
203                new ArrayList<TransferCapability>(scuTCs.size() + scpTCs.size());
204        tcs.addAll(scpTCs.values());
205        tcs.addAll(scuTCs.values());
206        return tcs;
207    }
208
209    /**
210     * Get the device that is identified by this application entity.
211     *
212     * @return The owning <code>Device</code>.
213     */
214    public Device getDevice() {
215        return device;
216    }
217
218    /**
219     * Set the device that is identified by this application entity.
220     *
221     * @param device The owning <code>Device</code>.
222     */
223    public void setDevice(Device device) {
224        if (device != null) {
225            if (this.device != null && this.device != device)
226                throw new IllegalStateException("already owned by " + this.device.getDeviceName());
227            for (Connection conn : connections)
228                if (conn.getDevice() != device)
229                    throw new IllegalStateException(conn + " not owned by " +
230                            device.getDeviceName());
231        }
232        this.device = device;
233    }
234
235    /**
236     * Get the AE title for this Network AE.
237     * <p/>
238     * <p/>
239     * Please note that there could also be alias AE titles for the same AE. You
240     * can get them via {@link #getAETitleAliases()}.
241     *
242     * @return A String containing the AE title.
243     */
244    public final String getAETitle() {
245        return AETitle;
246    }
247
248    /**
249     * Set the AE title for this Network AE.
250     *
251     * @param aet A String containing the AE title.
252     */
253    public void setAETitle(String aet) {
254        if (aet.isEmpty())
255            throw new IllegalArgumentException("AE title cannot be empty");
256        Device device = this.device;
257        if (device != null && this.AETitle != null)
258            device.removeApplicationEntity(this.AETitle);
259        this.AETitle = aet;
260        if (device != null)
261            device.addApplicationEntity(this);
262    }
263
264    /**
265     * Get the description of this network AE
266     *
267     * @return A String containing the description.
268     */
269    public final String getDescription() {
270        return description;
271    }
272
273    /**
274     * Set a description of this network AE.
275     *
276     * @param description A String containing the description.
277     */
278    public final void setDescription(String description) {
279        this.description = description;
280    }
281
282    /**
283     * Get any vendor information or configuration specific to this network AE.
284     *
285     * @return An Object of the vendor data.
286     */
287    public final byte[][] getVendorData() {
288        return vendorData;
289    }
290
291    /**
292     * Set any vendor information or configuration specific to this network AE
293     *
294     * @param vendorData An Object of the vendor data.
295     */
296    public final void setVendorData(byte[]... vendorData) {
297        this.vendorData = vendorData;
298    }
299
300    /**
301     * Get the locally defined names for a subset of related applications. E.g.
302     * neuroradiology.
303     *
304     * @return A String[] containing the names.
305     */
306    public String[] getApplicationClusters() {
307        return applicationClusters;
308    }
309
310    public void setApplicationClusters(String... clusters) {
311        applicationClusters = clusters;
312    }
313
314    /**
315     * Get the AE Title(s) that are preferred for initiating associations
316     * from this network AE.
317     *
318     * @return A String[] of the preferred called AE titles.
319     */
320    public String[] getPreferredCalledAETitles() {
321        return preferredCalledAETitles;
322    }
323
324    public void setPreferredCalledAETitles(String... aets) {
325        preferredCalledAETitles = aets;
326    }
327
328    /**
329     * Get the AE title(s) that are preferred for accepting associations by
330     * this network AE.
331     *
332     * @return A String[] containing the preferred calling AE titles.
333     */
334    public String[] getPreferredCallingAETitles() {
335        return preferredCallingAETitles;
336    }
337
338    public void setPreferredCallingAETitles(String... aets) {
339        preferredCallingAETitles = aets;
340    }
341
342    public String[] getAcceptedCallingAETitles() {
343        return acceptedCallingAETitlesSet.toArray(
344                new String[acceptedCallingAETitlesSet.size()]);
345    }
346
347    public void setAcceptedCallingAETitles(String... aets) {
348        acceptedCallingAETitlesSet.clear();
349        for (String name : aets)
350            acceptedCallingAETitlesSet.add(name);
351    }
352
353    public boolean isAcceptedCallingAETitle(String aet) {
354        return acceptedCallingAETitlesSet.isEmpty()
355                || acceptedCallingAETitlesSet.contains(aet);
356    }
357
358    /**
359     * Get the Character Set(s) supported by the Network AE for data sets it
360     * receives. The value shall be selected from the Defined Terms for Specific
361     * Character Set (0008,0005) in PS3.3. If no values are present, this
362     * implies that the Network AE supports only the default character
363     * repertoire (ISO IR 6).
364     *
365     * @return A String array of the supported character sets.
366     */
367    public String[] getSupportedCharacterSets() {
368        return supportedCharacterSets;
369    }
370
371    /**
372     * Set the Character Set(s) supported by the Network AE for data sets it
373     * receives. The value shall be selected from the Defined Terms for Specific
374     * Character Set (0008,0005) in PS3.3. If no values are present, this
375     * implies that the Network AE supports only the default character
376     * repertoire (ISO IR 6).
377     *
378     * @param characterSets A String array of the supported character sets.
379     */
380    public void setSupportedCharacterSets(String... characterSets) {
381        supportedCharacterSets = characterSets;
382    }
383
384    /**
385     * Determine whether or not this network AE can accept associations.
386     *
387     * @return A boolean value. True if the Network AE can accept associations,
388     * false otherwise.
389     */
390    public final boolean isAssociationAcceptor() {
391        return associationAcceptor;
392    }
393
394    /**
395     * Set whether or not this network AE can accept associations.
396     *
397     * @param acceptor A boolean value. True if the Network AE can accept
398     *                 associations, false otherwise.
399     */
400    public final void setAssociationAcceptor(boolean acceptor) {
401        this.associationAcceptor = acceptor;
402    }
403
404    /**
405     * Determine whether or not this network AE can initiate associations.
406     *
407     * @return A boolean value. True if the Network AE can accept associations,
408     * false otherwise.
409     */
410    public final boolean isAssociationInitiator() {
411        return associationInitiator;
412    }
413
414    /**
415     * Set whether or not this network AE can initiate associations.
416     *
417     * @param initiator A boolean value. True if the Network AE can accept
418     *                  associations, false otherwise.
419     */
420    public final void setAssociationInitiator(boolean initiator) {
421        this.associationInitiator = initiator;
422    }
423
424
425    /**
426     * Determine whether or not this network AE is installed on a network.
427     *
428     * @return A Boolean value. True if the AE is installed on a network. If not
429     * present, information about the installed status of the AE is
430     * inherited from the device
431     */
432    public boolean isInstalled() {
433        return device != null && device.isInstalled()
434                && (aeInstalled == null || aeInstalled.booleanValue());
435    }
436
437    public Boolean getAeInstalled() {
438        return aeInstalled;
439    }
440
441
442    /**
443     * Set whether or not this network AE is installed on a network.
444     *
445     * @param aeInstalled A Boolean value. True if the AE is installed on a network.
446     *                    If not present, information about the installed status of
447     *                    the AE is inherited from the device
448     */
449    public void setAeInstalled(Boolean aeInstalled) {
450        this.aeInstalled = aeInstalled;
451    }
452
453    public DimseRQHandler getDimseRQHandler() {
454        DimseRQHandler handler = dimseRQHandler;
455        if (handler != null)
456            return handler;
457
458        Device device = this.device;
459        return device != null
460                ? device.getDimseRQHandler()
461                : null;
462    }
463
464    public final void setDimseRQHandler(DimseRQHandler dimseRQHandler) {
465        this.dimseRQHandler = dimseRQHandler;
466    }
467
468    private void checkInstalled() {
469        if (!isInstalled())
470            throw new IllegalStateException("Not installed");
471    }
472
473    private void checkDevice() {
474        if (device == null)
475            throw new IllegalStateException("Not attached to Device");
476    }
477
478    void onDimseRQ(Association as, PresentationContext pc, Dimse cmd,
479                   Attributes cmdAttrs, PDVInputStream data) throws IOException {
480        DimseRQHandler tmp = getDimseRQHandler();
481        if (tmp == null) {
482            LOG.error("DimseRQHandler not initalized");
483            throw new AAbort();
484        }
485        tmp.onDimseRQ(as, pc, cmd, cmdAttrs, data);
486    }
487
488    public void addConnection(Connection conn) {
489        if (conn.getProtocol() != Connection.Protocol.DICOM)
490            throw new IllegalArgumentException(
491                    "protocol != DICOM - " + conn.getProtocol());
492
493        if (device != null && device != conn.getDevice())
494            throw new IllegalStateException(conn + " not contained by Device: " +
495                    device.getDeviceName());
496        connections.add(conn);
497    }
498
499    public boolean removeConnection(Connection conn) {
500        return connections.remove(conn);
501    }
502
503    public void setConnections(List<Connection> connections) {
504        this.connections.clear();
505        for (Connection connection : connections) addConnection(connection);
506    }
507
508    public List<Connection> getConnections() {
509        return connections;
510    }
511
512    public TransferCapability addTransferCapability(TransferCapability tc) {
513        tc.setApplicationEntity(this);
514        TransferCapability prev = (tc.getRole() == TransferCapability.Role.SCU
515                ? scuTCs : scpTCs).put(tc.getSopClass(), tc);
516        if (prev != null && prev != tc)
517            prev.setApplicationEntity(null);
518        return prev;
519    }
520
521    public TransferCapability removeTransferCapabilityFor(String sopClass,
522                                                          TransferCapability.Role role) {
523        TransferCapability tc = (role == TransferCapability.Role.SCU ? scuTCs : scpTCs)
524                .remove(sopClass);
525        if (tc != null)
526            tc.setApplicationEntity(null);
527        return tc;
528    }
529
530    public Collection<TransferCapability> getTransferCapabilitiesWithRole(
531            TransferCapability.Role role) {
532        return (role == TransferCapability.Role.SCU ? scuTCs : scpTCs).values();
533    }
534
535    public TransferCapability getTransferCapabilityFor(
536            String sopClass, TransferCapability.Role role) {
537        return (role == TransferCapability.Role.SCU ? scuTCs : scpTCs).get(sopClass);
538    }
539
540    protected PresentationContext negotiate(AAssociateRQ rq, AAssociateAC ac,
541                                            PresentationContext rqpc) {
542        String as = rqpc.getAbstractSyntax();
543        TransferCapability tc = roleSelection(rq, ac, as);
544        int pcid = rqpc.getPCID();
545        if (tc == null)
546            return new PresentationContext(pcid,
547                    PresentationContext.ABSTRACT_SYNTAX_NOT_SUPPORTED,
548                    rqpc.getTransferSyntax());
549
550        for (String ts : rqpc.getTransferSyntaxes())
551            if (tc.containsTransferSyntax(ts)) {
552                byte[] info = negotiate(rq.getExtNegotiationFor(as), tc);
553                if (info != null)
554                    ac.addExtendedNegotiation(new ExtendedNegotiation(as, info));
555                return new PresentationContext(pcid,
556                        PresentationContext.ACCEPTANCE, ts);
557            }
558
559        return new PresentationContext(pcid,
560                PresentationContext.TRANSFER_SYNTAX_NOT_SUPPORTED,
561                rqpc.getTransferSyntax());
562    }
563
564    private TransferCapability roleSelection(AAssociateRQ rq,
565                                             AAssociateAC ac, String asuid) {
566        RoleSelection rqrs = rq.getRoleSelectionFor(asuid);
567        if (rqrs == null)
568            return getTC(scpTCs, asuid, rq);
569
570        RoleSelection acrs = ac.getRoleSelectionFor(asuid);
571        if (acrs != null)
572            return getTC(acrs.isSCU() ? scpTCs : scuTCs, asuid, rq);
573
574        TransferCapability tcscu = null;
575        TransferCapability tcscp = null;
576        boolean scu = rqrs.isSCU()
577                && (tcscp = getTC(scpTCs, asuid, rq)) != null;
578        boolean scp = rqrs.isSCP()
579                && (tcscu = getTC(scuTCs, asuid, rq)) != null;
580        ac.addRoleSelection(new RoleSelection(asuid, scu, scp));
581        return scu ? tcscp : tcscu;
582    }
583
584    private TransferCapability getTC(Map<String, TransferCapability> tcs,
585                                     String asuid, AAssociateRQ rq) {
586        TransferCapability tc = tcs.get(asuid);
587        if (tc != null)
588            return tc;
589
590        CommonExtendedNegotiation commonExtNeg =
591                rq.getCommonExtendedNegotiationFor(asuid);
592        if (commonExtNeg != null) {
593            for (String cuid : commonExtNeg.getRelatedGeneralSOPClassUIDs()) {
594                tc = tcs.get(cuid);
595                if (tc != null)
596                    return tc;
597            }
598            tc = tcs.get(commonExtNeg.getServiceClassUID());
599            if (tc != null)
600                return tc;
601        }
602
603        return tcs.get("*");
604    }
605
606    private byte[] negotiate(ExtendedNegotiation exneg, TransferCapability tc) {
607        if (exneg == null)
608            return null;
609
610        StorageOptions storageOptions = tc.getStorageOptions();
611        if (storageOptions != null)
612            return storageOptions.toExtendedNegotiationInformation();
613
614        EnumSet<QueryOption> queryOptions = tc.getQueryOptions();
615        if (queryOptions != null) {
616            EnumSet<QueryOption> commonOpts = QueryOption.toOptions(exneg);
617            commonOpts.retainAll(queryOptions);
618            return QueryOption.toExtendedNegotiationInformation(commonOpts);
619        }
620        return null;
621    }
622
623    public Association connect(Connection local, Connection remote, AAssociateRQ rq)
624            throws IOException, InterruptedException, IncompatibleConnectionException, GeneralSecurityException {
625        checkDevice();
626        checkInstalled();
627        if (rq.getCallingAET() == null)
628            rq.setCallingAET(AETitle);
629        rq.setMaxOpsInvoked(local.getMaxOpsInvoked());
630        rq.setMaxOpsPerformed(local.getMaxOpsPerformed());
631        rq.setMaxPDULength(local.getReceivePDULength());
632
633        final Socket sock = local.connect(remote); // automatically closes the socket in case an exception is thrown
634
635        Association as;
636        try {
637            as = new Association(this, local, sock);
638        } catch (final IOException e) {
639            LOG.warn("Failed to open new association, will close underlying socket");
640            local.close(sock);
641            throw e;
642        }
643        try {
644            as.write(rq);
645            as.waitForLeaving(State.Sta5);
646        } catch (final IOException e) {
647            LOG.warn("{}: Failed to write A-ASSOCIATE-RQ, will abort association", as.toString());
648            as.abort();
649            throw e;
650        } catch (final InterruptedException e) {
651            LOG.warn("{}: Interrupted while waiting to leave state Sta 5, will abort association", as.toString());
652            as.abort();
653            throw e;
654        }
655        return as;
656    }
657
658    public Association connect(Connection remote, AAssociateRQ rq)
659            throws IOException, InterruptedException, IncompatibleConnectionException, GeneralSecurityException {
660        return connect(findCompatibelConnection(remote), remote, rq);
661    }
662
663    public Connection findCompatibelConnection(Connection remoteConn)
664            throws IncompatibleConnectionException {
665        for (Connection conn : connections)
666            if (conn.isInstalled() && conn.isCompatible(remoteConn))
667                return conn;
668        throw new IncompatibleConnectionException(
669                "No compatible connection to " + remoteConn + " available on " + this);
670    }
671
672    public CompatibleConnection findCompatibelConnection(ApplicationEntity remote)
673            throws IncompatibleConnectionException {
674        CompatibleConnection cc = null;
675        for (Connection remoteConn : remote.connections)
676            if (remoteConn.isInstalled() && remoteConn.isServer())
677                for (Connection conn : connections)
678                    if (conn.isInstalled() && conn.isCompatible(remoteConn)) {
679                        if (cc == null
680                                || conn.isTls()
681                                || conn.getProtocol() == Connection.Protocol.SYSLOG_TLS)
682                            cc = new CompatibleConnection(conn, remoteConn);
683                    }
684        if (cc == null)
685            throw new IncompatibleConnectionException(
686                    "No compatible connection to " + remote + " available on " + this);
687        return cc;
688    }
689
690    public Association connect(ApplicationEntity remote, AAssociateRQ rq)
691            throws IOException, InterruptedException, IncompatibleConnectionException, GeneralSecurityException {
692        CompatibleConnection cc = findCompatibelConnection(remote);
693        if (rq.getCalledAET() == null)
694            rq.setCalledAET(remote.getAETitle());
695        return connect(cc.getLocalConnection(), cc.getRemoteConnection(), rq);
696    }
697
698    @Override
699    public String toString() {
700        return promptTo(new StringBuilder(512), "").toString();
701    }
702
703    public StringBuilder promptTo(StringBuilder sb, String indent) {
704        String indent2 = indent + "  ";
705        StringUtils.appendLine(sb, indent, "ApplicationEntity[title: ", AETitle);
706        StringUtils.appendLine(sb, indent2, "alias titles: ", AETitleAliases);
707        StringUtils.appendLine(sb, indent2, "desc: ", description);
708        StringUtils.appendLine(sb, indent2, "acceptor: ", associationAcceptor);
709        StringUtils.appendLine(sb, indent2, "initiator: ", associationInitiator);
710        StringUtils.appendLine(sb, indent2, "installed: ", getAeInstalled());
711        for (Connection conn : connections)
712            conn.promptTo(sb, indent2).append(StringUtils.LINE_SEPARATOR);
713        for (TransferCapability tc : getTransferCapabilities())
714            tc.promptTo(sb, indent2).append(StringUtils.LINE_SEPARATOR);
715        return sb.append(indent).append(']');
716    }
717
718    void reconfigure(ApplicationEntity src) {
719        setApplicationEntityAttributes(src);
720        device.reconfigureConnections(connections, src.connections);
721        reconfigureTransferCapabilities(src);
722        reconfigureAEExtensions(src);
723    }
724
725    private void reconfigureTransferCapabilities(ApplicationEntity src) {
726        scuTCs.clear();
727        scuTCs.putAll(src.scuTCs);
728        scpTCs.clear();
729        scpTCs.putAll(src.scpTCs);
730    }
731
732    private void reconfigureAEExtensions(ApplicationEntity from) {
733        for (Iterator<Class<? extends AEExtension>> it =
734             extensions.keySet().iterator(); it.hasNext(); ) {
735            if (!from.extensions.containsKey(it.next()))
736                it.remove();
737        }
738        for (AEExtension src : from.extensions.values()) {
739            Class<? extends AEExtension> clazz = src.getClass();
740            AEExtension ext = extensions.get(clazz);
741            if (ext == null)
742                try {
743                    addAEExtension(ext = clazz.newInstance());
744                } catch (Exception e) {
745                    throw new RuntimeException(
746                            "Failed to instantiate " + clazz.getName(), e);
747                }
748            ext.reconfigure(src);
749        }
750    }
751
752    protected void setApplicationEntityAttributes(ApplicationEntity from) {
753        setOlockHash(from.olockHash);
754        setDescription(from.description);
755        setAETitleAliases(from.getAETitleAliases());
756        setVendorData(from.vendorData);
757        setApplicationClusters(from.applicationClusters);
758        setPreferredCalledAETitles(from.preferredCalledAETitles);
759        setPreferredCallingAETitles(from.preferredCallingAETitles);
760        setAcceptedCallingAETitles(from.getAcceptedCallingAETitles());
761        setSupportedCharacterSets(from.supportedCharacterSets);
762        setAssociationAcceptor(from.associationAcceptor);
763        setAssociationInitiator(from.associationInitiator);
764        setAeInstalled(from.aeInstalled);
765        setUuid(from.getUuid());
766    }
767
768    public Set<String> getAcceptedCallingAETitlesSet() {
769        return acceptedCallingAETitlesSet;
770    }
771
772    public void setAcceptedCallingAETitlesSet(Set<String> acceptedCallingAETitlesSet) {
773        this.acceptedCallingAETitlesSet.clear();
774        if (acceptedCallingAETitlesSet != null)
775            this.acceptedCallingAETitlesSet.addAll(acceptedCallingAETitlesSet);
776    }
777
778    public void addAEExtension(AEExtension ext) {
779        Class<? extends AEExtension> clazz = ext.getClass();
780        if (extensions.containsKey(clazz))
781            throw new IllegalStateException(
782                    "already contains AE Extension:" + clazz);
783
784        ext.setApplicationEntity(this);
785        extensions.put(clazz, ext);
786    }
787
788    public boolean removeAEExtension(AEExtension ext) {
789        if (extensions.remove(ext.getClass()) == null)
790            return false;
791
792        ext.setApplicationEntity(null);
793        return true;
794    }
795
796    public String getOlockHash() {
797        return olockHash;
798    }
799
800    public void setOlockHash(String olockHash) {
801        this.olockHash = olockHash;
802    }
803
804    public Collection<AEExtension> listAEExtensions() {
805        return extensions.values();
806    }
807
808    @SuppressWarnings("unchecked")
809    public <T extends AEExtension> T getAEExtension(Class<T> clazz) {
810        return (T) extensions.get(clazz);
811    }
812
813    public <T extends AEExtension> T getAEExtensionNotNull(Class<T> clazz) {
814        T aeExt = getAEExtension(clazz);
815        if (aeExt == null)
816            throw new IllegalStateException("No " + clazz.getName()
817                    + " configured for AE: " + AETitle);
818        return aeExt;
819    }
820
821    public boolean supportsTransferCapability(
822            TransferCapability transferCapability, boolean onlyAbstractSyntax) {
823        TransferCapability matchingTC = this.getTransferCapabilityFor(
824                transferCapability.getSopClass(), transferCapability.getRole());
825        if (matchingTC == null)
826            return false;
827        else
828            for (String ts : transferCapability.getTransferSyntaxes())
829                if (!matchingTC.containsTransferSyntax(ts)
830                        && !onlyAbstractSyntax)
831                    return false;
832
833        return true;
834    }
835
836}