/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is part of dcm4che, an implementation of DICOM(TM) in * Java(TM), hosted at https://github.com/gunterze/dcm4che. * * The Initial Developer of the Original Code is * Agfa Healthcare. * Portions created by the Initial Developer are Copyright (C) 2011 * the Initial Developer. All Rights Reserved. * * Contributor(s): * See @authors listed below * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ package org.dcm4che3.net; import org.dcm4che3.conf.core.api.ConfigurableClass; import org.dcm4che3.conf.core.api.ConfigurableProperty; import org.dcm4che3.conf.core.api.ConfigurableProperty.ConfigurablePropertyType; import org.dcm4che3.conf.core.api.ConfigurableProperty.Tag; import org.dcm4che3.conf.core.api.LDAP; import org.dcm4che3.conf.core.api.Parent; import org.dcm4che3.data.Attributes; import org.dcm4che3.net.pdu.*; import org.dcm4che3.util.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.Serializable; import java.net.Socket; import java.security.GeneralSecurityException; import java.util.*; /** * DICOM Part 15, Annex H compliant description of a DICOM network service. * <p/> * A Network AE is an application entity that provides services on a network. A * Network AE will have the 16 same functional capability regardless of the * particular network connection used. If there are functional differences based * on selected network connection, then these are separate Network AEs. If there * are 18 functional differences based on other internal structures, then these * are separate Network AEs. * * @author Gunter Zeilinger <gunterze@gmail.com> */ @LDAP(objectClasses = {"dcmNetworkAE", "dicomNetworkAE"}, distinguishingField = "dicomAETitle") @ConfigurableClass(referable = true) public class ApplicationEntity implements Serializable { private static final long serialVersionUID = 3883790997057469573L; protected static final Logger LOG = LoggerFactory.getLogger(ApplicationEntity.class); @ConfigurableProperty(name = "dicomAETitle", tags = Tag.PRIMARY) private String AETitle; @ConfigurableProperty(type = ConfigurablePropertyType.UUID, description = "An immutable unique identifier") private String uuid = UUID.randomUUID().toString(); @ConfigurableProperty(name = "dicomDescription") private String description; @ConfigurableProperty(type = ConfigurablePropertyType.OptimisticLockingHash) private String olockHash; @ConfigurableProperty(name = "dicomVendorData") private byte[][] vendorData = {}; @ConfigurableProperty(name = "dicomApplicationCluster") private String[] applicationClusters = {}; @ConfigurableProperty(name = "dicomPreferredCalledAETitle") private String[] preferredCalledAETitles = {}; @ConfigurableProperty(name = "dicomPreferredCallingAETitle") private String[] preferredCallingAETitles = {}; @ConfigurableProperty(name = "dicomSupportedCharacterSet") private String[] supportedCharacterSets = {}; @ConfigurableProperty(name = "dicomInstalled") private Boolean aeInstalled; @ConfigurableProperty(name = "dcmAcceptedCallingAETitle") private final Set<String> acceptedCallingAETitlesSet = new LinkedHashSet<String>(); // Connections are dereferenced by DicomConfiguration @ConfigurableProperty(name = "dicomNetworkConnectionReference", collectionOfReferences = true, tags = Tag.PRIMARY) private final List<Connection> connections = new ArrayList<Connection>(1); /** * "Proxy" property, actually forwards everything to scuTCs and scpTCs in its setter/getter */ @LDAP(noContainerNode = true) @ConfigurableProperty(name = "dcmTransferCapability", description = "DICOM Transfer Capabilities", tags = Tag.PRIMARY) private Collection<TransferCapability> transferCapabilities; // populated/collected by transferCapabilities' setter/getter private final Map<String, TransferCapability> scuTCs = new TreeMap<String, TransferCapability>(); // populated/collected by transferCapabilities' setter/getter private final Map<String, TransferCapability> scpTCs = new TreeMap<String, TransferCapability>(); @ConfigurableProperty(name = "aeExtensions", isExtensionsProperty = true) private Map<Class<? extends AEExtension>, AEExtension> extensions = new HashMap<Class<? extends AEExtension>, AEExtension>(); @ConfigurableProperty(name = "dicomAssociationAcceptor") private boolean associationAcceptor = true; @ConfigurableProperty(name = "dicomAssociationInitiator") private boolean associationInitiator = true; @ConfigurableProperty(name = "dcmAETitleAliases", label = "Aliases (alternative AE titles)") private List<String> AETitleAliases = new ArrayList<String>(); @Parent private Device device; private transient DimseRQHandler dimseRQHandler; public ApplicationEntity() { } public ApplicationEntity(String aeTitle) { setAETitle(aeTitle); } public List<String> getAETitleAliases() { return new ArrayList<String>(AETitleAliases); } public void setAETitleAliases(List<String> AETitleAliases) { this.AETitleAliases = AETitleAliases; } public Map<Class<? extends AEExtension>, AEExtension> getExtensions() { return extensions; } public void setExtensions(Map<Class<? extends AEExtension>, AEExtension> extensions) { this.extensions = extensions; } public String getUuid() { return uuid; } public void setUuid(String uuid) { this.uuid = uuid; } public void setTransferCapabilities(Collection<TransferCapability> transferCapabilities) { scpTCs.clear(); scuTCs.clear(); for (TransferCapability tc : transferCapabilities) { tc.setApplicationEntity(this); switch (tc.getRole()) { case SCP: scpTCs.put(tc.getSopClass(), tc); break; case SCU: scuTCs.put(tc.getSopClass(), tc); } } } public Collection<TransferCapability> getTransferCapabilities() { ArrayList<TransferCapability> tcs = new ArrayList<TransferCapability>(scuTCs.size() + scpTCs.size()); tcs.addAll(scpTCs.values()); tcs.addAll(scuTCs.values()); return tcs; } /** * Get the device that is identified by this application entity. * * @return The owning <code>Device</code>. */ public Device getDevice() { return device; } /** * Set the device that is identified by this application entity. * * @param device The owning <code>Device</code>. */ public void setDevice(Device device) { if (device != null) { if (this.device != null && this.device != device) throw new IllegalStateException("already owned by " + this.device.getDeviceName()); for (Connection conn : connections) if (conn.getDevice() != device) throw new IllegalStateException(conn + " not owned by " + device.getDeviceName()); } this.device = device; } /** * Get the AE title for this Network AE. * <p/> * <p/> * Please note that there could also be alias AE titles for the same AE. You * can get them via {@link #getAETitleAliases()}. * * @return A String containing the AE title. */ public final String getAETitle() { return AETitle; } /** * Set the AE title for this Network AE. * * @param aet A String containing the AE title. */ public void setAETitle(String aet) { if (aet.isEmpty()) throw new IllegalArgumentException("AE title cannot be empty"); Device device = this.device; if (device != null && this.AETitle != null) device.removeApplicationEntity(this.AETitle); this.AETitle = aet; if (device != null) device.addApplicationEntity(this); } /** * Get the description of this network AE * * @return A String containing the description. */ public final String getDescription() { return description; } /** * Set a description of this network AE. * * @param description A String containing the description. */ public final void setDescription(String description) { this.description = description; } /** * Get any vendor information or configuration specific to this network AE. * * @return An Object of the vendor data. */ public final byte[][] getVendorData() { return vendorData; } /** * Set any vendor information or configuration specific to this network AE * * @param vendorData An Object of the vendor data. */ public final void setVendorData(byte[]... vendorData) { this.vendorData = vendorData; } /** * Get the locally defined names for a subset of related applications. E.g. * neuroradiology. * * @return A String[] containing the names. */ public String[] getApplicationClusters() { return applicationClusters; } public void setApplicationClusters(String... clusters) { applicationClusters = clusters; } /** * Get the AE Title(s) that are preferred for initiating associations * from this network AE. * * @return A String[] of the preferred called AE titles. */ public String[] getPreferredCalledAETitles() { return preferredCalledAETitles; } public void setPreferredCalledAETitles(String... aets) { preferredCalledAETitles = aets; } /** * Get the AE title(s) that are preferred for accepting associations by * this network AE. * * @return A String[] containing the preferred calling AE titles. */ public String[] getPreferredCallingAETitles() { return preferredCallingAETitles; } public void setPreferredCallingAETitles(String... aets) { preferredCallingAETitles = aets; } public String[] getAcceptedCallingAETitles() { return acceptedCallingAETitlesSet.toArray( new String[acceptedCallingAETitlesSet.size()]); } public void setAcceptedCallingAETitles(String... aets) { acceptedCallingAETitlesSet.clear(); for (String name : aets) acceptedCallingAETitlesSet.add(name); } public boolean isAcceptedCallingAETitle(String aet) { return acceptedCallingAETitlesSet.isEmpty() || acceptedCallingAETitlesSet.contains(aet); } /** * Get the Character Set(s) supported by the Network AE for data sets it * receives. The value shall be selected from the Defined Terms for Specific * Character Set (0008,0005) in PS3.3. If no values are present, this * implies that the Network AE supports only the default character * repertoire (ISO IR 6). * * @return A String array of the supported character sets. */ public String[] getSupportedCharacterSets() { return supportedCharacterSets; } /** * Set the Character Set(s) supported by the Network AE for data sets it * receives. The value shall be selected from the Defined Terms for Specific * Character Set (0008,0005) in PS3.3. If no values are present, this * implies that the Network AE supports only the default character * repertoire (ISO IR 6). * * @param characterSets A String array of the supported character sets. */ public void setSupportedCharacterSets(String... characterSets) { supportedCharacterSets = characterSets; } /** * Determine whether or not this network AE can accept associations. * * @return A boolean value. True if the Network AE can accept associations, * false otherwise. */ public final boolean isAssociationAcceptor() { return associationAcceptor; } /** * Set whether or not this network AE can accept associations. * * @param acceptor A boolean value. True if the Network AE can accept * associations, false otherwise. */ public final void setAssociationAcceptor(boolean acceptor) { this.associationAcceptor = acceptor; } /** * Determine whether or not this network AE can initiate associations. * * @return A boolean value. True if the Network AE can accept associations, * false otherwise. */ public final boolean isAssociationInitiator() { return associationInitiator; } /** * Set whether or not this network AE can initiate associations. * * @param initiator A boolean value. True if the Network AE can accept * associations, false otherwise. */ public final void setAssociationInitiator(boolean initiator) { this.associationInitiator = initiator; } /** * Determine whether or not this network AE is installed on a network. * * @return A Boolean value. True if the AE is installed on a network. If not * present, information about the installed status of the AE is * inherited from the device */ public boolean isInstalled() { return device != null && device.isInstalled() && (aeInstalled == null || aeInstalled.booleanValue()); } public Boolean getAeInstalled() { return aeInstalled; } /** * Set whether or not this network AE is installed on a network. * * @param aeInstalled A Boolean value. True if the AE is installed on a network. * If not present, information about the installed status of * the AE is inherited from the device */ public void setAeInstalled(Boolean aeInstalled) { this.aeInstalled = aeInstalled; } public DimseRQHandler getDimseRQHandler() { DimseRQHandler handler = dimseRQHandler; if (handler != null) return handler; Device device = this.device; return device != null ? device.getDimseRQHandler() : null; } public final void setDimseRQHandler(DimseRQHandler dimseRQHandler) { this.dimseRQHandler = dimseRQHandler; } private void checkInstalled() { if (!isInstalled()) throw new IllegalStateException("Not installed"); } private void checkDevice() { if (device == null) throw new IllegalStateException("Not attached to Device"); } void onDimseRQ(Association as, PresentationContext pc, Dimse cmd, Attributes cmdAttrs, PDVInputStream data) throws IOException { DimseRQHandler tmp = getDimseRQHandler(); if (tmp == null) { LOG.error("DimseRQHandler not initalized"); throw new AAbort(); } tmp.onDimseRQ(as, pc, cmd, cmdAttrs, data); } public void addConnection(Connection conn) { if (conn.getProtocol() != Connection.Protocol.DICOM) throw new IllegalArgumentException( "protocol != DICOM - " + conn.getProtocol()); if (device != null && device != conn.getDevice()) throw new IllegalStateException(conn + " not contained by Device: " + device.getDeviceName()); connections.add(conn); } public boolean removeConnection(Connection conn) { return connections.remove(conn); } public void setConnections(List<Connection> connections) { this.connections.clear(); for (Connection connection : connections) addConnection(connection); } public List<Connection> getConnections() { return connections; } public TransferCapability addTransferCapability(TransferCapability tc) { tc.setApplicationEntity(this); TransferCapability prev = (tc.getRole() == TransferCapability.Role.SCU ? scuTCs : scpTCs).put(tc.getSopClass(), tc); if (prev != null && prev != tc) prev.setApplicationEntity(null); return prev; } public TransferCapability removeTransferCapabilityFor(String sopClass, TransferCapability.Role role) { TransferCapability tc = (role == TransferCapability.Role.SCU ? scuTCs : scpTCs) .remove(sopClass); if (tc != null) tc.setApplicationEntity(null); return tc; } public Collection<TransferCapability> getTransferCapabilitiesWithRole( TransferCapability.Role role) { return (role == TransferCapability.Role.SCU ? scuTCs : scpTCs).values(); } public TransferCapability getTransferCapabilityFor( String sopClass, TransferCapability.Role role) { return (role == TransferCapability.Role.SCU ? scuTCs : scpTCs).get(sopClass); } protected PresentationContext negotiate(AAssociateRQ rq, AAssociateAC ac, PresentationContext rqpc) { String as = rqpc.getAbstractSyntax(); TransferCapability tc = roleSelection(rq, ac, as); int pcid = rqpc.getPCID(); if (tc == null) return new PresentationContext(pcid, PresentationContext.ABSTRACT_SYNTAX_NOT_SUPPORTED, rqpc.getTransferSyntax()); for (String ts : rqpc.getTransferSyntaxes()) if (tc.containsTransferSyntax(ts)) { byte[] info = negotiate(rq.getExtNegotiationFor(as), tc); if (info != null) ac.addExtendedNegotiation(new ExtendedNegotiation(as, info)); return new PresentationContext(pcid, PresentationContext.ACCEPTANCE, ts); } return new PresentationContext(pcid, PresentationContext.TRANSFER_SYNTAX_NOT_SUPPORTED, rqpc.getTransferSyntax()); } private TransferCapability roleSelection(AAssociateRQ rq, AAssociateAC ac, String asuid) { RoleSelection rqrs = rq.getRoleSelectionFor(asuid); if (rqrs == null) return getTC(scpTCs, asuid, rq); RoleSelection acrs = ac.getRoleSelectionFor(asuid); if (acrs != null) return getTC(acrs.isSCU() ? scpTCs : scuTCs, asuid, rq); TransferCapability tcscu = null; TransferCapability tcscp = null; boolean scu = rqrs.isSCU() && (tcscp = getTC(scpTCs, asuid, rq)) != null; boolean scp = rqrs.isSCP() && (tcscu = getTC(scuTCs, asuid, rq)) != null; ac.addRoleSelection(new RoleSelection(asuid, scu, scp)); return scu ? tcscp : tcscu; } private TransferCapability getTC(Map<String, TransferCapability> tcs, String asuid, AAssociateRQ rq) { TransferCapability tc = tcs.get(asuid); if (tc != null) return tc; CommonExtendedNegotiation commonExtNeg = rq.getCommonExtendedNegotiationFor(asuid); if (commonExtNeg != null) { for (String cuid : commonExtNeg.getRelatedGeneralSOPClassUIDs()) { tc = tcs.get(cuid); if (tc != null) return tc; } tc = tcs.get(commonExtNeg.getServiceClassUID()); if (tc != null) return tc; } return tcs.get("*"); } private byte[] negotiate(ExtendedNegotiation exneg, TransferCapability tc) { if (exneg == null) return null; StorageOptions storageOptions = tc.getStorageOptions(); if (storageOptions != null) return storageOptions.toExtendedNegotiationInformation(); EnumSet<QueryOption> queryOptions = tc.getQueryOptions(); if (queryOptions != null) { EnumSet<QueryOption> commonOpts = QueryOption.toOptions(exneg); commonOpts.retainAll(queryOptions); return QueryOption.toExtendedNegotiationInformation(commonOpts); } return null; } public Association connect(Connection local, Connection remote, AAssociateRQ rq) throws IOException, InterruptedException, IncompatibleConnectionException, GeneralSecurityException { checkDevice(); checkInstalled(); if (rq.getCallingAET() == null) rq.setCallingAET(AETitle); rq.setMaxOpsInvoked(local.getMaxOpsInvoked()); rq.setMaxOpsPerformed(local.getMaxOpsPerformed()); rq.setMaxPDULength(local.getReceivePDULength()); final Socket sock = local.connect(remote); // automatically closes the socket in case an exception is thrown Association as; try { as = new Association(this, local, sock); } catch (final IOException e) { LOG.warn("Failed to open new association, will close underlying socket"); local.close(sock); throw e; } try { as.write(rq); as.waitForLeaving(State.Sta5); } catch (final IOException e) { LOG.warn("{}: Failed to write A-ASSOCIATE-RQ, will abort association", as.toString()); as.abort(); throw e; } catch (final InterruptedException e) { LOG.warn("{}: Interrupted while waiting to leave state Sta 5, will abort association", as.toString()); as.abort(); throw e; } return as; } public Association connect(Connection remote, AAssociateRQ rq) throws IOException, InterruptedException, IncompatibleConnectionException, GeneralSecurityException { return connect(findCompatibelConnection(remote), remote, rq); } public Connection findCompatibelConnection(Connection remoteConn) throws IncompatibleConnectionException { for (Connection conn : connections) if (conn.isInstalled() && conn.isCompatible(remoteConn)) return conn; throw new IncompatibleConnectionException( "No compatible connection to " + remoteConn + " available on " + this); } public CompatibleConnection findCompatibelConnection(ApplicationEntity remote) throws IncompatibleConnectionException { CompatibleConnection cc = null; for (Connection remoteConn : remote.connections) if (remoteConn.isInstalled() && remoteConn.isServer()) for (Connection conn : connections) if (conn.isInstalled() && conn.isCompatible(remoteConn)) { if (cc == null || conn.isTls() || conn.getProtocol() == Connection.Protocol.SYSLOG_TLS) cc = new CompatibleConnection(conn, remoteConn); } if (cc == null) throw new IncompatibleConnectionException( "No compatible connection to " + remote + " available on " + this); return cc; } public Association connect(ApplicationEntity remote, AAssociateRQ rq) throws IOException, InterruptedException, IncompatibleConnectionException, GeneralSecurityException { CompatibleConnection cc = findCompatibelConnection(remote); if (rq.getCalledAET() == null) rq.setCalledAET(remote.getAETitle()); return connect(cc.getLocalConnection(), cc.getRemoteConnection(), rq); } @Override public String toString() { return promptTo(new StringBuilder(512), "").toString(); } public StringBuilder promptTo(StringBuilder sb, String indent) { String indent2 = indent + " "; StringUtils.appendLine(sb, indent, "ApplicationEntity[title: ", AETitle); StringUtils.appendLine(sb, indent2, "alias titles: ", AETitleAliases); StringUtils.appendLine(sb, indent2, "desc: ", description); StringUtils.appendLine(sb, indent2, "acceptor: ", associationAcceptor); StringUtils.appendLine(sb, indent2, "initiator: ", associationInitiator); StringUtils.appendLine(sb, indent2, "installed: ", getAeInstalled()); for (Connection conn : connections) conn.promptTo(sb, indent2).append(StringUtils.LINE_SEPARATOR); for (TransferCapability tc : getTransferCapabilities()) tc.promptTo(sb, indent2).append(StringUtils.LINE_SEPARATOR); return sb.append(indent).append(']'); } void reconfigure(ApplicationEntity src) { setApplicationEntityAttributes(src); device.reconfigureConnections(connections, src.connections); reconfigureTransferCapabilities(src); reconfigureAEExtensions(src); } private void reconfigureTransferCapabilities(ApplicationEntity src) { scuTCs.clear(); scuTCs.putAll(src.scuTCs); scpTCs.clear(); scpTCs.putAll(src.scpTCs); } private void reconfigureAEExtensions(ApplicationEntity from) { for (Iterator<Class<? extends AEExtension>> it = extensions.keySet().iterator(); it.hasNext(); ) { if (!from.extensions.containsKey(it.next())) it.remove(); } for (AEExtension src : from.extensions.values()) { Class<? extends AEExtension> clazz = src.getClass(); AEExtension ext = extensions.get(clazz); if (ext == null) try { addAEExtension(ext = clazz.newInstance()); } catch (Exception e) { throw new RuntimeException( "Failed to instantiate " + clazz.getName(), e); } ext.reconfigure(src); } } protected void setApplicationEntityAttributes(ApplicationEntity from) { setOlockHash(from.olockHash); setDescription(from.description); setAETitleAliases(from.getAETitleAliases()); setVendorData(from.vendorData); setApplicationClusters(from.applicationClusters); setPreferredCalledAETitles(from.preferredCalledAETitles); setPreferredCallingAETitles(from.preferredCallingAETitles); setAcceptedCallingAETitles(from.getAcceptedCallingAETitles()); setSupportedCharacterSets(from.supportedCharacterSets); setAssociationAcceptor(from.associationAcceptor); setAssociationInitiator(from.associationInitiator); setAeInstalled(from.aeInstalled); setUuid(from.getUuid()); } public Set<String> getAcceptedCallingAETitlesSet() { return acceptedCallingAETitlesSet; } public void setAcceptedCallingAETitlesSet(Set<String> acceptedCallingAETitlesSet) { this.acceptedCallingAETitlesSet.clear(); if (acceptedCallingAETitlesSet != null) this.acceptedCallingAETitlesSet.addAll(acceptedCallingAETitlesSet); } public void addAEExtension(AEExtension ext) { Class<? extends AEExtension> clazz = ext.getClass(); if (extensions.containsKey(clazz)) throw new IllegalStateException( "already contains AE Extension:" + clazz); ext.setApplicationEntity(this); extensions.put(clazz, ext); } public boolean removeAEExtension(AEExtension ext) { if (extensions.remove(ext.getClass()) == null) return false; ext.setApplicationEntity(null); return true; } public String getOlockHash() { return olockHash; } public void setOlockHash(String olockHash) { this.olockHash = olockHash; } public Collection<AEExtension> listAEExtensions() { return extensions.values(); } @SuppressWarnings("unchecked") public <T extends AEExtension> T getAEExtension(Class<T> clazz) { return (T) extensions.get(clazz); } public <T extends AEExtension> T getAEExtensionNotNull(Class<T> clazz) { T aeExt = getAEExtension(clazz); if (aeExt == null) throw new IllegalStateException("No " + clazz.getName() + " configured for AE: " + AETitle); return aeExt; } public boolean supportsTransferCapability( TransferCapability transferCapability, boolean onlyAbstractSyntax) { TransferCapability matchingTC = this.getTransferCapabilityFor( transferCapability.getSopClass(), transferCapability.getRole()); if (matchingTC == null) return false; else for (String ts : transferCapability.getTransferSyntaxes()) if (!matchingTC.containsTransferSyntax(ts) && !onlyAbstractSyntax) return false; return true; } }