diff --git a/pom.xml b/pom.xml index ba1ba45..399cba0 100644 --- a/pom.xml +++ b/pom.xml @@ -1,250 +1,285 @@ - + - 4.0.0 + xmlns="http://maven.apache.org/POM/4.0.0" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 - net.tammon - sip - 0.4.2 - jar + net.tammon + sip + 0.4.3 + jar - ${project.groupId}:${project.artifactId} - This is a library to communicate to sercos devices over the Sercos Internet Protocol (SIP) + ${project.groupId}:${project.artifactId} + This is a library to communicate to sercos devices over the Sercos Internet Protocol (SIP) - https://github.com/tammon/sip4java + https://github.com/tammon/sip4java - - UTF-8 - 1.2.71 - true - + + UTF-8 + 1.2.71 + true + - - - MIT License - http://www.opensource.org/licenses/mit-license.php - - + + + MIT License + http://www.opensource.org/licenses/mit-license.php + + - - - Tammo Schwindt - dev@tammon.net - Tammon - https://github.com/tammon - - + + + Tammo Schwindt + dev@tammon.net + Tammon + https://github.com/tammon + + - - scm:git:git://github.com/tammon/sip4java.git - scm:git:ssh://github.com:tammon/sip4java.git - http://github.com/tammon/sip4java/tree/master - + + scm:git:git://github.com/tammon/sip4java.git + scm:git:ssh://github.com:tammon/sip4java.git + http://github.com/tammon/sip4java/tree/master + - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + - - - - kotlin-maven-plugin - org.jetbrains.kotlin - ${kotlin.version} - - - compile - - compile - - - - ${project.basedir}/src/main/kotlin - ${project.basedir}/src/main/java - - - - - test-compile - - test-compile - - - - ${project.basedir}/src/test/kotlin - ${project.basedir}/src/test/java - - - - - + + + + kotlin-maven-plugin + org.jetbrains.kotlin + ${kotlin.version} + + + compile + + compile + + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/main/java + + + + + test-compile + + test-compile + + + + ${project.basedir}/src/test/kotlin + ${project.basedir}/src/test/java + + + + + - - org.apache.maven.plugins - maven-compiler-plugin - - 1.8 - 1.8 - - 3.7.0 - - - - default-compile - none - - - - default-testCompile - none - - - java-compile - compile - - compile - - - - java-test-compile - test-compile - - testCompile - - - - - - maven-surefire-plugin - 2.22.1 - - - org.junit.platform - junit-platform-surefire-provider - 1.3.1 - - - org.junit.jupiter - junit-jupiter-engine - 5.3.1 - - - - - + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + 3.8.0 + + + + default-compile + none + + + + default-testCompile + none + + + java-compile + compile + + compile + + + + java-test-compile + test-compile + + testCompile + + + + + + maven-surefire-plugin + 2.22.1 + + + org.junit.platform + junit-platform-surefire-provider + 1.3.1 + + + org.junit.jupiter + junit-jupiter-engine + 5.3.2 + + + + + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + + org.jetbrains.kotlin + + + kotlin-maven-plugin + + + [1.2.71,) + + + test-compile + + + + + + + + + + + + + - - - release - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.7 - true - - ossrh - https://oss.sonatype.org/ - true - - - - org.apache.maven.plugins - maven-source-plugin - 2.2.1 - - - attach-sources - - jar-no-fork - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.9.1 - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.5 - - - sign-artifacts - verify - - sign - - - - - - - - + + + release + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.7 + true + + ossrh + https://oss.sonatype.org/ + true + + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.5 + + + sign-artifacts + verify + + sign + + + + + + + + - - - org.jetbrains.kotlin - kotlin-stdlib - ${kotlin.version} - - - org.junit.jupiter - junit-jupiter-api - 5.3.1 - test - - - org.slf4j - slf4j-api - 1.7.15 - - + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + org.junit.jupiter + junit-jupiter-api + 5.3.2 + test + + + org.slf4j + slf4j-api + 1.7.15 + + + + org.slf4j + slf4j-simple + 1.7.25 + test + + diff --git a/src/main/java/net/tammon/sip/TCPConnection.java b/src/main/java/net/tammon/sip/TCPConnection.java index 94e88f6..a42ff7c 100644 --- a/src/main/java/net/tammon/sip/TCPConnection.java +++ b/src/main/java/net/tammon/sip/TCPConnection.java @@ -39,465 +39,578 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** - * The TCPConnection class implements the SipConnection Interface and creates a - * sip connection via the TCP/IP protocol + * The TCPConnection class implements the SipConnection Interface and creates a sip connection via the TCP/IP protocol */ public class TCPConnection implements SipConnection { - private InetAddress ipAddress; - private int maxDelay, leaseTimeout, busyTimeout, sipPort, sipVersion; - private int transactionId = 0; - private boolean connected = false; - private List supportedMessages; - private Socket socketConnection; - private DataOutputStream dataOutputStream; - private DataInputStream dataInputStream; - private ScheduledExecutorService executorService; - - public TCPConnection(Properties properties) throws SipException { - connect(properties); - } - - private void connect(Properties properties) throws SipSocketTimeoutException, SipException { - try { - String host = null; - if (properties.containsKey("host")) { - host = properties.getProperty("host"); - } - this.ipAddress = InetAddress.getByName((host == null) ? properties.getProperty("driveIp") : host); - } catch (UnknownHostException e) { - throw new SipInternalException( - "Cannot resolve hostname. This is probably due to a misspelled hostname or bad dns configuration of host", - e); - - } - - this.sipPort = Integer.parseInt(properties.getProperty("sipPort")); - this.leaseTimeout = Integer.parseInt(properties.getProperty("leaseTimeout")); - this.busyTimeout = Integer.parseInt(properties.getProperty("busyTimeout")); - this.maxDelay = Integer.parseInt(properties.getProperty("maxDelay")); - this.sipVersion = Integer.parseInt(properties.getProperty("sipVersion")); - - this.connectSocket(); - this.connectSip(); - - boolean keepAlive = (boolean) properties.get("keepAlive"); - if (keepAlive) - this.restartKeepAliveTimer(); - } - - /** - * Establishes a TCP connection to a sercos device with given IP Address - *

- * This connection can have a keepAlive flag which will maintain the SIP - * connection even if no request gets sent for a longer duration than the - * leaseTimeout. This will prevent the drive of closing the socket connection - * after the lease timeout. If a keep alive is used it is mandatory to call the - * disconnect() method to stop the keep alive functionality. If disconnect() is - * not called the tcp connection will prevent your program from exiting. - * - * @param host - * domain name or IP Address of the drive - * @param keepAlive - * flag if the connection should stay alive even if no request are - * sent - * @throws SipException - * in case of communication problems - */ - public TCPConnection(String host, boolean keepAlive) throws SipException { - Properties properties = getDefaultProperties(); - properties.put("keepAlive", keepAlive); - if (host != null) { - properties.put("host", host); - } - connect(properties); - } - - /** - * Establishes a TCP connection to a sercos device with the standard IP address - * of an IndraDrive (192.168.0.1) This connection has no keepAlive and will - * timeout if no input request comes in for longer then the standard - * leaseTimeout of 10s - * - * @throws SipException - * in case of communication problems - */ - public TCPConnection() throws SipException { - this(null, false); - } - - /** - * Establishes a TCP connection to a sercos device with the given IP address - * This connection has no keepAlive and will timeout if no input request comes - * in for longer then the standard leaseTimeout of 10s - * - * @param host - * domain name or IP Address of the drive - * @throws SipException - * in case of communication problems - */ - public TCPConnection(String host) throws SipException { - this(host, false); - } - - /** - * @return sipDefault properties file as {@link Properties} Object - */ - private Properties getDefaultProperties() { - InputStream inputStream = ClassLoader.getSystemResourceAsStream("sipDefault.properties"); - Properties properties = new Properties(); - try { - properties.load(inputStream); - inputStream.close(); - } catch (IOException e) { - throw new SipInternalException("Problem occurred while trying to load sipDefault.properties", e); - } - return properties; - } - - /** - * Establishes a new sip connection by reconnecting the socket and the sercos - * device. Resets the list of supported messages - * - * @throws SipSocketTimeoutException - * in case of a socket timeout - */ - private synchronized void connectSocket() throws SipSocketTimeoutException { - this.transactionId = 0; - this.socketConnection = new Socket(); - try { - this.socketConnection.connect(new InetSocketAddress(this.ipAddress, this.sipPort), busyTimeout); - this.dataOutputStream = new DataOutputStream(this.socketConnection.getOutputStream()); - this.dataInputStream = new DataInputStream(this.socketConnection.getInputStream()); - this.supportedMessages = null; - } catch (SocketTimeoutException e) { - throw new SipSocketTimeoutException( - // "Drive does not respond to socket request. Probably wrong IP or not on - // network..."); - "Drive does not respond. Probably Drive is not online."); - } catch (IOException e) { - // throw new SipInternalException("An internal S/IP Exception occured", e); - throw new SipInternalException( - "Probably Drive is not online or the IP:'" + this.ipAddress.toString() + "' is wrong.", e); - } - } - - /** - * Generates synchronized a new transaction id for a new sip interaction by - * increasing the current transaction id by one. - * - * @return new transaction id - */ - private synchronized int getNewTransactionId() { - return this.transactionId++; - } - - /** - * This method is the general abstraction for all sip tcp communication between - * the library and the sercos device. It takes tcp request and response packets - * of the sip library. It sends the request and returns the response. The tcp - * send and receive logic is fully synchronized to avoid multiple instances and - * therefor requests at a time. - * - * @param request - * sip request tcp packet - * @param response - * sip response tcp packet type (generally empty initialized object) - * @return sip response tcp packet of the given type with the packet data set to - * the object - * @throws SipException - * in case of communication problems - */ - private synchronized Response getTcpResponse(Request request, Class response) throws SipException { - if (this.isSupported(request.getMessageType())) - throw new SipServiceNotSupportedException("The requested operation " + request.getClass().getSimpleName() - + " is not in the drive's list of supported messages"); - - sendDataToServer(request.getTcpMsgAsByteArray()); - - byte[] rawResponse = getRawResponseFromSocket(); - return getResponse(rawResponse, request, response); - } - - /** - * Returns an instantiated object of specified response class with the data of - * the raw response. In addition to that this method checks if the data is valid - * and matches the request. - * - * @param rawResponse - * raw data from socket - * @param request - * the request object that belongs to the response - * @param responseClass - * specifies the response class which will be used as instantiated - * object for return type - * @return response with data from type responseClass - * @throws SipProtocolException - * in case the sercos device threw an communication exception (e.g. - * invalid request) or in case of a wrong transaction id - * @throws SipServiceNotSupportedException - * in case the sercos device does not support the requested message - * type - */ - private Response getResponse(byte[] rawResponse, Request request, Class responseClass) - throws SipProtocolException, SipServiceNotSupportedException { - try { - Response response = (Response) responseClass.newInstance(); - - Head header = new Head(rawResponse); - - // Check if we got the right response to our request - if (header.getTransactionId() != request.getTransactionId()) - throw new SipProtocolException( - "The response transaction ID " + response.getPacketHead().getTransactionId() - + " doesn't match the request transaction ID " + request.getTransactionId()); - - // Check if Drive threw an communication exception - if (header.getMessageType() == 67) { - ExceptionResponse exceptionResponse = new ExceptionResponse(rawResponse); - if (exceptionResponse.getCommonErrorCode() == CommonErrorCodes.SERVICESPECIFIC) - throw new SipProtocolException("Drive threw Communication Exception." - + ((exceptionResponse.getCommonErrorCode() == CommonErrorCodes.SERVICESPECIFIC) - ? (" SIP-SpecificErrorCode: " + exceptionResponse.getSpecificErrorCode()) - : (" SIP-CommonErrorCode: " + exceptionResponse.getCommonErrorCode()))); - - if (exceptionResponse.getCommonErrorCode() == CommonErrorCodes.UNKNOWN_MESSAGE_TYPE) - throw new SipProtocolException("Service not supported."); - } - // TODO busy response einfügen - - if (header.getMessageType() == response.getMessageType()) - response.setData(rawResponse); - else - throw new SipInternalException("Invalid Message Type Response"); - return response; - } catch (InstantiationException | IllegalAccessException e) { - throw new SipInternalException("Invalid Response Class Type. Cannot instantiate object.", e); - } catch (IOException e) { - throw new SipInternalException( - "An internal error occurred during conversion of raw data to response object.", e); - } - } - - /** - * Reads the data from the open Socket - * - * @return the raw data of the socket as byte array - * @throws SipCommunicationException - * in case of any problem occurs during socket communication - */ - private byte[] getRawResponseFromSocket() throws SipCommunicationException { - try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { - byte[] buffer = new byte[10024]; - int readLength; - do { - readLength = dataInputStream.read(buffer); - if (readLength >= 0) { - outputStream.write(buffer, 0, readLength + 1); - } - } while (readLength >= 10024); - - return outputStream.toByteArray(); - } catch (IOException e) { - e.printStackTrace();// hinzugefügt von Philip Weis - throw new SipCommunicationException("Cannot read from Socket", e); - } - } - - /** - * sends a byte array via the open socket of this Sip connection - * - * @param data - * the raw data to send - * @throws SipCommunicationException - * in case of any problem occurs during socket communication - */ - private void sendDataToServer(byte[] data) throws SipCommunicationException { - try { - this.dataOutputStream.write(data); - this.dataOutputStream.flush(); - } catch (IOException e) { - throw new SipCommunicationException("Cannot write output stream data to S/IP device", e); - } - } - - /** - * checks whether a message type is supported by the connected S/IP device - * - * @param messageType - * message type to check - * @return message type supported - */ - private boolean isSupported(int messageType) { - return !Objects.isNull(this.supportedMessages) && !this.supportedMessages.contains(messageType); - } - - /** - * This method sends a sip connection request to the sercos device and handles - * the incoming response - * - * @throws SipException - * in case of communication problems - */ - private void connectSip() throws SipException { - Connect request = new Connect(this.getNewTransactionId(), this.sipVersion, this.busyTimeout, this.leaseTimeout); - ConnectResponse response = (ConnectResponse) this.getTcpResponse(request, ConnectResponse.class); - synchronized (this) { - this.supportedMessages = IntStream.of(response.getSupportedMessageTypes()).boxed().collect(Collectors.toList()); - this.connected = true; - } - } - - /** - * Checks if the sercos device responds to a ping sip message - * - * @return true if the device responds, false if it doesn't - */ - private boolean respondsToPing() { - Ping ping = new Ping(this.getNewTransactionId()); - try { - this.getTcpResponse(ping, Pong.class); - return true; // hinzugefügt von Philip Weis - // } catch (SipException e) { - } catch (Exception e) { // abgeändert von Philip Weis - e.printStackTrace(); // hinzugefügt von Philip Weis - return false; // hinzugefügt von Philip Weis - } - // return true; //abgeändert von Philip Weis - } - - /** - * This method is the general abstraction of all public ReadOnlyData methods. It - * actually creates the request and response bodies and triggers the TCP send - * and receive. - * - * @param slaveIndex - * the slave index of the sercos device (default: 0) - * @param slaveExtension - * the slave extentension of the sercos device (default: 0) - * @param idn - * the 16-bit or 32-bit identifier of the parameter one wants to read - * (e.g. "P-0-0100" or "S-0-0100.1.1") - * @return the {@link ReadOnlyDataResponse} which is received after the tcp - * request - * @throws SipException - * if any communication or data handling problem occurs - */ - public Data readData(int slaveIndex, int slaveExtension, String idn) throws SipException { - ReadOnlyData request = new ReadOnlyData(this.getNewTransactionId(), (short) slaveIndex, (short) slaveExtension, - idn); - ReadOnlyDataResponse response = (ReadOnlyDataResponse) this.getTcpResponse(request, ReadOnlyDataResponse.class); - - return response.getData(); - } - - /** - * Checks whether or not the TCP connection to the sercos slave is still - * connected - * - * @return true if the connection is alive - */ - @Override - public boolean isConnected() { - synchronized (this) { - return connected && this.socketConnection.isConnected(); - } - } - - /** - * Returns a list of the supported message types of the sercos device. This list - * is initially sent by the device during the sip connection process. - * - * @return the list of supported message types - */ - @Override - public List getSupportedMessages() { - synchronized (this) { - return supportedMessages; - } - } - - /** - * Returns the IP address of the sercos device which is used by this TCP - * connection. - * - * @return IP address of sercos device as {@link InetAddress} - */ - @Override - public InetAddress getIpAddress() { - return ipAddress; - } - - /** - * Returns the used sercos device port of the current sip connection - * - * @return used sercos device port - */ - @Override - public int getSipPort() { - return sipPort; - } - - /** - * Returns the used version of the Sercos Internet Protocol - * - * @return version of Sercos Internet Protocol - */ - @Override - public int getSipVersion() { - return sipVersion; - } - - private void restartKeepAliveTimer() { - if (Objects.isNull(this.executorService)) - this.executorService = Executors.newScheduledThreadPool(1); - else - this.executorService.shutdownNow(); - final long period = Math.round(this.leaseTimeout * 0.7); - executorService.scheduleAtFixedRate(this::respondsToPing, 0, period, TimeUnit.MILLISECONDS); - } - - /** - * Stops the keep alive loop and closes the socket connection to the sercos - * device - */ - @Override - public void disconnect() { - - close(); - - if (executorService != null) { - try { - executorService.shutdownNow(); - } catch (Exception e) { - // No exception handling here - } - } - - try { - synchronized (this) { - if (socketConnection != null) { - socketConnection.close(); - socketConnection = null; - } - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - private void close() { - try { - if (dataInputStream != null) { - dataInputStream.close(); - dataInputStream = null; - } - if (dataOutputStream != null) { - dataOutputStream.close(); - dataOutputStream = null; - } - } catch (Throwable e) { - e.printStackTrace(); + + private static Logger _logger = LoggerFactory.getLogger("net.tammon.sip"); + + private InetAddress ipAddress; + private int maxDelay, leaseTimeout, busyTimeout, sipPort, sipVersion; + private int transactionId = 0; + private boolean connected = false; + private List supportedMessages; + private Socket socketConnection; + private DataOutputStream dataOutputStream; + private DataInputStream dataInputStream; + private ScheduledExecutorService executorService; + + public TCPConnection(Properties properties) throws SipException { + connect(properties); + } + + private void connect(Properties properties) throws SipSocketTimeoutException, SipException { + try { + String host = null; + if (properties.containsKey("host")) { + host = properties.getProperty("host"); + } + this.ipAddress = InetAddress.getByName((host == null) ? properties.getProperty("driveIp") : host); + } catch (UnknownHostException e) { + throw new SipInternalException( + "Cannot resolve hostname. This is probably due to a misspelled hostname or bad dns configuration of host", + e); + + } + + this.sipPort = Integer.parseInt(properties.getProperty("sipPort")); + this.leaseTimeout = Integer.parseInt(properties.getProperty("leaseTimeout")); + this.busyTimeout = Integer.parseInt(properties.getProperty("busyTimeout")); + this.maxDelay = Integer.parseInt(properties.getProperty("maxDelay")); + this.sipVersion = Integer.parseInt(properties.getProperty("sipVersion")); + + this.connectSocket(); + this.connectSip(); + + boolean keepAlive = (boolean) properties.get("keepAlive"); + if (keepAlive) + this.restartKeepAliveTimer(); + } + + /** + * Establishes a TCP connection to a sercos device with given IP Address + *

+ * This connection can have a keepAlive flag which will maintain the SIP connection even if no request gets sent for + * a longer duration than the leaseTimeout. This will prevent the drive of closing the socket connection after the + * lease timeout. If a keep alive is used it is mandatory to call the disconnect() method to stop the keep alive + * functionality. If disconnect() is not called the tcp connection will prevent your program from exiting. + * + * @param host + * domain name or IP Address of the drive + * @param keepAlive + * flag if the connection should stay alive even if no request are sent + * @throws SipException + * in case of communication problems + */ + public TCPConnection(String host, boolean keepAlive) throws SipException { + Properties properties = getDefaultProperties(); + properties.put("keepAlive", keepAlive); + if (host != null) { + properties.put("host", host); + } + connect(properties); + } + + /** + * Establishes a TCP connection to a sercos device with the standard IP address of an IndraDrive (192.168.0.1) This + * connection has no keepAlive and will timeout if no input request comes in for longer then the standard + * leaseTimeout of 10s + * + * @throws SipException + * in case of communication problems + */ + public TCPConnection() throws SipException { + this(null, false); + } + + /** + * Establishes a TCP connection to a sercos device with the given IP address This connection has no keepAlive and + * will timeout if no input request comes in for longer then the standard leaseTimeout of 10s + * + * @param host + * domain name or IP Address of the drive + * @throws SipException + * in case of communication problems + */ + public TCPConnection(String host) throws SipException { + this(host, false); + } + + /** + * @return sipDefault properties file as {@link Properties} Object + */ + private Properties getDefaultProperties() { + InputStream inputStream = ClassLoader.getSystemResourceAsStream("sipDefault.properties"); + Properties properties = new Properties(); + try { + properties.load(inputStream); + inputStream.close(); + } catch (IOException e) { + throw new SipInternalException("Problem occurred while trying to load sipDefault.properties", e); + } + return properties; + } + + /** + * Establishes a new sip connection by reconnecting the socket and the sercos device. Resets the list of supported + * messages + * + * @throws SipSocketTimeoutException + * in case of a socket timeout + */ + private synchronized void connectSocket() throws SipSocketTimeoutException { + this.transactionId = 0; + this.socketConnection = new Socket(); + try { + this.socketConnection.connect(new InetSocketAddress(this.ipAddress, this.sipPort), busyTimeout); + this.socketConnection.setSoTimeout(busyTimeout); + this.dataOutputStream = new DataOutputStream(this.socketConnection.getOutputStream()); + this.dataInputStream = new DataInputStream(this.socketConnection.getInputStream()); + this.supportedMessages = null; + } catch (SocketTimeoutException e) { + throw new SipSocketTimeoutException( + // "Drive does not respond to socket request. Probably wrong IP or not on + // network..."); + "Drive does not respond. Probably Drive is not online."); + } catch (IOException e) { + // throw new SipInternalException("An internal S/IP Exception occured", e); + throw new SipInternalException( + "Probably Drive is not online or the IP:'" + this.ipAddress.toString() + "' is wrong.", e); + } + } + + /** + * Generates synchronized a new transaction id for a new sip interaction by increasing the current transaction id by + * one. + * + * @return new transaction id + */ + private synchronized int getNewTransactionId() { + return this.transactionId++; + } + + /** + * This method is the general abstraction for all sip tcp communication between the library and the sercos device. + * It takes tcp request and response packets of the sip library. It sends the request and returns the response. The + * tcp send and receive logic is fully synchronized to avoid multiple instances and therefor requests at a time. + * + * @param request + * sip request tcp packet + * @param response + * sip response tcp packet type (generally empty initialized object) + * @return sip response tcp packet of the given type with the packet data set to the object + * @throws SipException + * in case of communication problems + */ + @SuppressWarnings("rawtypes") + private synchronized Response getTcpResponse(Request request, Class response) throws SipException { + if (this.isSupported(request.getMessageType())) + throw new SipServiceNotSupportedException("The requested operation " + request.getClass().getSimpleName() + + " is not in the drive's list of supported messages"); + + sendDataToServer(request.getTcpMsgAsByteArray()); + + byte[] rawResponse; + if (isOnlyDataResponse(request)) { + rawResponse = getRawResponseFromSocket(request); + } else { + rawResponse = getRawResponseFromSocket(); + } + return getResponse(printlnTel(rawResponse), request, response); + } + + private byte[] getRawResponseFromSocket(Request request) + throws SipCommunicationException, SipProtocolException { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + + waitOnData(); + + int readLength; + + // Read Header + byte[] rawheader = new byte[Head.getFixLength()]; + readLength = dataInputStream.read(rawheader, 0, Head.getFixLength()); + + Head header = new Head(rawheader); + + // Check if we got the right response to our request + if (header.getTransactionId() != request.getTransactionId()) { + throw new SipProtocolException( + "The response transaction ID " + header.getTransactionId() + + " doesn't match the request transaction ID " + request.getTransactionId()); + } + + outputStream.write(rawheader, 0, readLength); + + checkAndCreateSipException(outputStream, header); + + byte[] rawReadOnlyData = new byte[ReadOnlyDataResponse.getFixLength()]; + readLength = dataInputStream.read(rawReadOnlyData, 0, ReadOnlyDataResponse.getFixLength()); + outputStream.write(rawReadOnlyData, 0, readLength); + + int datalen = ReadOnlyDataResponse.getLengthOfData(rawReadOnlyData); + int revBytes = 0; + byte[] buffer = new byte[1024]; + while (datalen > revBytes) { + readLength = dataInputStream.read(buffer); + if (readLength >= 0) { + outputStream.write(buffer, 0, readLength); + revBytes += readLength; + } + } + // array checks on arr.length-1 + byte[] dummy = { 0 }; + outputStream.write(dummy, 0, dummy.length); + return outputStream.toByteArray(); + } catch (IOException e) { + // e.printStackTrace();// hinzugefügt von Philip Weis + throw new SipCommunicationException("Cannot read from Socket", e); + } + } + + /** + * Reads the data from the open Socket + * + * @return the raw data of the socket as byte array + * @throws SipCommunicationException + * in case of any problem occurs during socket communication + */ + private byte[] getRawResponseFromSocket() throws SipCommunicationException { + + waitOnData(); + + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + byte[] buffer = new byte[10024]; + int readLength; + do { + readLength = dataInputStream.read(buffer); + if (readLength >= 0) { + outputStream.write(buffer, 0, readLength + 1); + } + } + while (readLength >= 10024); + + return outputStream.toByteArray(); + } catch (IOException e) { + // e.printStackTrace();// hinzugefügt von Philip Weis + throw new SipCommunicationException("Cannot read from Socket", e); + } + } + + /** + * Returns an instantiated object of specified response class with the data of the raw response. In addition to that + * this method checks if the data is valid and matches the request. + * + * @param rawResponse + * raw data from socket + * @param request + * the request object that belongs to the response + * @param responseClass + * specifies the response class which will be used as instantiated object for return type + * @return response with data from type responseClass + * @throws SipProtocolException + * in case the sercos device threw an communication exception (e.g. invalid request) or in case of a + * wrong transaction id + * @throws SipServiceNotSupportedException + * in case the sercos device does not support the requested message type + */ + @SuppressWarnings("rawtypes") + private Response getResponse(byte[] rawResponse, Request request, Class responseClass) + throws SipProtocolException, SipServiceNotSupportedException { + try { + + Head header = new Head(rawResponse); + + // Check if we got the right response to our request + if (header.getTransactionId() != request.getTransactionId()) + throw new SipProtocolException( + "The response transaction ID " + header.getTransactionId() + + " doesn't match the request transaction ID " + request.getTransactionId()); + + // Check if Drive threw an communication exception + if (header.getMessageType() == Head.MSG_EXCEPTION) { + createSipException(rawResponse); + } + + Response response = (Response) responseClass.newInstance(); + + // TODO busy response einfügen + + if (header.getMessageType() == response.getMessageType()) + response.setData(rawResponse); + else + throw new SipInternalException("Invalid Message Type Response"); + return response; + } catch (InstantiationException | IllegalAccessException e) { + throw new SipInternalException("Invalid Response Class Type. Cannot instantiate object.", e); + } catch (IOException e) { + throw new SipInternalException( + "An internal error occurred during conversion of raw data to response object.", e); + } + } + + private void checkAndCreateSipException(ByteArrayOutputStream outputStream, Head header) + throws IOException, SipProtocolException { + int readLength; + // Check if Drive threw an communication exception + if (header.getMessageType() == Head.MSG_EXCEPTION) { + byte [] rawException = new byte[1024]; + readLength = dataInputStream.read(rawException); + outputStream.write(rawException, 0, readLength+1); + createSipException(outputStream.toByteArray()); + } + } + + private void createSipException(byte [] excption) throws IOException, SipProtocolException { + ExceptionResponse exceptionResponse = new ExceptionResponse(excption); + if (exceptionResponse.getCommonErrorCode() == CommonErrorCodes.SERVICESPECIFIC) + throw new SipProtocolException("Drive threw Communication Exception." + + ((exceptionResponse.getCommonErrorCode() == CommonErrorCodes.SERVICESPECIFIC) + ? (" SIP-SpecificErrorCode: " + exceptionResponse.getSpecificErrorCode()) + : (" SIP-CommonErrorCode: " + exceptionResponse.getCommonErrorCode()))); + + if (exceptionResponse.getCommonErrorCode() == CommonErrorCodes.UNKNOWN_MESSAGE_TYPE) + throw new SipProtocolException("Service not supported."); + } + + private void waitOnData() { + int repeat = 100; //max. wait 1 Sec. + try { + while (dataInputStream.available() == 0 && repeat > 0) { + _logger.info("wait: no values"); + try { + Thread.sleep(10); + repeat--; + } catch (InterruptedException e) { + } + } + } catch (IOException e) { + } + } + + private boolean isOnlyDataResponse(Request request) { + return request.getMessageType() == ReadOnlyData.MSG_READ_ONLY_DATA; + } + + /** + * sends a byte array via the open socket of this Sip connection + * + * @param data + * the raw data to send + * @throws SipCommunicationException + * in case of any problem occurs during socket communication + */ + private void sendDataToServer(byte[] data) throws SipCommunicationException { + try { + this.dataOutputStream.write(data); + this.dataOutputStream.flush(); + } catch (IOException e) { + throw new SipCommunicationException("Cannot write output stream data to S/IP device", e); + } + } + + /** + * checks whether a message type is supported by the connected S/IP device + * + * @param messageType + * message type to check + * @return message type supported + */ + private boolean isSupported(int messageType) { + return !Objects.isNull(this.supportedMessages) && !this.supportedMessages.contains(messageType); + } + + /** + * This method sends a sip connection request to the sercos device and handles the incoming response + * + * @throws SipException + * in case of communication problems + */ + private void connectSip() throws SipException { + Connect request = new Connect(this.getNewTransactionId(), this.sipVersion, this.busyTimeout, this.leaseTimeout); + ConnectResponse response = (ConnectResponse) this.getTcpResponse(request, ConnectResponse.class); + synchronized (this) { + this.supportedMessages = + IntStream.of(response.getSupportedMessageTypes()).boxed().collect(Collectors.toList()); + this.connected = true; + } + } + + /** + * Checks if the sercos device responds to a ping sip message + * + * @return true if the device responds, false if it doesn't + */ + private boolean respondsToPing() { + Ping ping = new Ping(this.getNewTransactionId()); + try { + this.getTcpResponse(ping, Pong.class); + return true; // hinzugefügt von Philip Weis + // } catch (SipException e) { + } catch (Exception e) { // abgeändert von Philip Weis + e.printStackTrace(); // hinzugefügt von Philip Weis + return false; // hinzugefügt von Philip Weis + } + // return true; //abgeändert von Philip Weis + } + + /** + * This method is the general abstraction of all public ReadOnlyData methods. It actually creates the request and + * response bodies and triggers the TCP send and receive. + * + * @param slaveIndex + * the slave index of the sercos device (default: 0) + * @param slaveExtension + * the slave extentension of the sercos device (default: 0) + * @param idn + * the 16-bit or 32-bit identifier of the parameter one wants to read (e.g. "P-0-0100" or "S-0-0100.1.1") + * @return the {@link ReadOnlyDataResponse} which is received after the tcp request + * @throws SipException + * if any communication or data handling problem occurs + */ + public Data readData(int slaveIndex, int slaveExtension, String idn) throws SipException { + ReadOnlyData request = new ReadOnlyData(this.getNewTransactionId(), (short) slaveIndex, (short) slaveExtension, + idn); + ReadOnlyDataResponse response = (ReadOnlyDataResponse) this.getTcpResponse(request, ReadOnlyDataResponse.class); + + return response.getData(); + } + + /** + * Checks whether or not the TCP connection to the sercos slave is still connected + * + * @return true if the connection is alive + */ + @Override + public boolean isConnected() { + synchronized (this) { + return connected && this.socketConnection.isConnected(); + } + } + + /** + * Returns a list of the supported message types of the sercos device. This list is initially sent by the device + * during the sip connection process. + * + * @return the list of supported message types + */ + @Override + public List getSupportedMessages() { + synchronized (this) { + return supportedMessages; + } + } + + /** + * Returns the IP address of the sercos device which is used by this TCP connection. + * + * @return IP address of sercos device as {@link InetAddress} + */ + @Override + public InetAddress getIpAddress() { + return ipAddress; + } + + /** + * Returns the used sercos device port of the current sip connection + * + * @return used sercos device port + */ + @Override + public int getSipPort() { + return sipPort; + } + + /** + * Returns the used version of the Sercos Internet Protocol + * + * @return version of Sercos Internet Protocol + */ + @Override + public int getSipVersion() { + return sipVersion; + } + + private void restartKeepAliveTimer() { + if (Objects.isNull(this.executorService)) + this.executorService = Executors.newScheduledThreadPool(1); + else + this.executorService.shutdownNow(); + final long period = Math.round(this.leaseTimeout * 0.7); + executorService.scheduleAtFixedRate(this::respondsToPing, 0, period, TimeUnit.MILLISECONDS); + } + + /** + * Stops the keep alive loop and closes the socket connection to the sercos device + */ + @Override + public void disconnect() { + + close(); + + if (executorService != null) { + try { + executorService.shutdownNow(); + } catch (Exception e) { + // No exception handling here + } + } + + try { + synchronized (this) { + if (socketConnection != null) { + socketConnection.close(); + socketConnection = null; + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void close() { + try { + if (dataInputStream != null) { + dataInputStream.close(); + dataInputStream = null; + } + if (dataOutputStream != null) { + dataOutputStream.close(); + dataOutputStream = null; + } + } catch (Throwable e) { + e.printStackTrace(); + } + } + + private static byte [] printlnTel(byte[] rawData) { + printTelegram(rawData); + return rawData; + } + + public static void printTelegram(byte[] rawData) { + if (null == rawData) { + return; + } + if (!_logger.isInfoEnabled()) { + return; + } + StringBuilder b = new StringBuilder(); + b.append("data-len: "); + b.append(rawData.length); + b.append(System.lineSeparator()); + int max = rawData.length > 256 ? 256 : rawData.length-1; + for (int i=0; i 15 && i % 16 == 0) { + b.append(System.lineSeparator()); + } + } + + _logger.info(b.toString()); } - } } diff --git a/src/main/java/net/tammon/sip/packets/Head.java b/src/main/java/net/tammon/sip/packets/Head.java index 6e8806b..f5be0ac 100644 --- a/src/main/java/net/tammon/sip/packets/Head.java +++ b/src/main/java/net/tammon/sip/packets/Head.java @@ -29,7 +29,7 @@ import java.io.IOException; public final class Head { - private static int MSG_EXCEPTION = 67; + public static int MSG_EXCEPTION = 67; private int transactionId; private int messageType; @@ -60,8 +60,6 @@ public int getMessageType() { return messageType; } - - public void setMessageType(int messageType) { this.messageType = messageType; } @@ -69,4 +67,9 @@ public void setMessageType(int messageType) { public byte[] getDataAsByteArray (){ return Data.getByteArray(this.transactionId, this.messageType); } + + public static int getFixLength() { + // sizeof(transaction id) + sizefo(messagetype) + return (Integer.SIZE + Integer.SIZE)/8; + } } diff --git a/src/main/java/net/tammon/sip/packets/ReadOnlyData.java b/src/main/java/net/tammon/sip/packets/ReadOnlyData.java index d814907..93607d1 100644 --- a/src/main/java/net/tammon/sip/packets/ReadOnlyData.java +++ b/src/main/java/net/tammon/sip/packets/ReadOnlyData.java @@ -27,13 +27,13 @@ public class ReadOnlyData extends AbstractPacket implements Request { - private static final int messageType = 71; + public static final int MSG_READ_ONLY_DATA = 71; private final short slaveIndex; private final short slaveExtension; private final Idn idn; public ReadOnlyData(int transactionId, short slaveIndex, short slaveExtension, String idn) throws IllegalArgumentException { - this.head = new Head(transactionId, messageType); + this.head = new Head(transactionId, MSG_READ_ONLY_DATA); this.slaveIndex = slaveIndex; this.slaveExtension = slaveExtension; this.idn = new Idn(idn); @@ -49,6 +49,6 @@ public byte[] getTcpMsgAsByteArray() { @Override public int getMessageType() { - return messageType; + return MSG_READ_ONLY_DATA; } } diff --git a/src/main/java/net/tammon/sip/packets/ReadOnlyDataResponse.java b/src/main/java/net/tammon/sip/packets/ReadOnlyDataResponse.java index c61f58e..02fe345 100644 --- a/src/main/java/net/tammon/sip/packets/ReadOnlyDataResponse.java +++ b/src/main/java/net/tammon/sip/packets/ReadOnlyDataResponse.java @@ -34,9 +34,14 @@ import java.io.InvalidClassException; import java.util.Arrays; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class ReadOnlyDataResponse extends AbstractPacket implements Response { - private final static int messageType = 72; + private static Logger _logger = LoggerFactory.getLogger("net.tammon.sip"); + + private final static int messageType = ReadOnlyData.MSG_READ_ONLY_DATA+1; private Data data; /** @@ -56,6 +61,7 @@ private void setBodyData(byte[] rawBodyData) throws IOException, TypeNotSupporte byte[] rawData = new byte[lengthOfData]; ((FilterInputStream)data).read(rawData); this.data = new Data(rawData, dataAttribute); + _logger.info("ReadOnly Data: raw-len={}; data-len={}", rawBodyData.length, lengthOfData); } /** @@ -84,4 +90,16 @@ public void setData(byte[] rawData) { throw new SipInternalException("Cannot set data of received S/IP packets", e); } } + + public static int getFixLength() { + // sizeof(attribute) + sizefo(data length) + return (Integer.SIZE + Integer.SIZE)/8; + } + + public static int getLengthOfData(byte[] rawReadOnlyData) throws IOException { + DataInput data = DataStreamFactory.getLittleEndianDataInputStream(rawReadOnlyData); + int lengthOfData = data.readInt(); //attribute + lengthOfData = data.readInt(); //length + return lengthOfData; + } } diff --git a/src/test/java/net/tammon/sip/TCPConnectionTest.java b/src/test/java/net/tammon/sip/TCPConnectionTest.java new file mode 100644 index 0000000..dfbd210 --- /dev/null +++ b/src/test/java/net/tammon/sip/TCPConnectionTest.java @@ -0,0 +1,71 @@ +package net.tammon.sip; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Properties; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import net.tammon.sip.exceptions.SipException; +import net.tammon.sip.packets.Data; + +class TCPConnectionTest { + + @Disabled + @Test + void testReadDataSimple() { + Properties props = new Properties(); + props.setProperty("host", "192.168.0.1"); + props.setProperty("sipPort", "35021"); + props.setProperty("leaseTimeout", "20000"); + props.setProperty("busyTimeout", "10000"); + props.setProperty("maxDelay", "500"); + props.setProperty("sipVersion", "1"); + props.put("keepAlive", false); + + try { + TCPConnection tcp = new TCPConnection(props); + Data data = tcp.readData(0, 0, "S-0-0000"); + assertNotNull(data); + tcp.disconnect(); + } catch (SipException e) { + e.printStackTrace(); + } + } + + + @Disabled + @Test + void testReadData() { + Properties props = new Properties(); + props.setProperty("host", "192.168.0.1"); + props.setProperty("sipPort", "35021"); + props.setProperty("leaseTimeout", "20000"); + props.setProperty("busyTimeout", "10000"); + props.setProperty("maxDelay", "500"); + props.setProperty("sipVersion", "1"); + props.put("keepAlive", false); + + try { + TCPConnection tcp = new TCPConnection(props); + Data data = tcp.readData(0, 0, "S-0-0017"); + assertNotNull(data); + data = tcp.readData(0, 0, "S-0-0000"); + assertNotNull(data); + tcp.disconnect(); + } catch (SipException e) { + e.printStackTrace(); + } + } + + @Disabled + @Test + void testReadData2() { + Properties props = new Properties(); + + props.setProperty("sipVersion", "1"); + props.put("keepAlive", false); + } + +}