Import of the IEC 60870 component This change adds an IEC 60870 camel component based on the IEC 60870 implementation of Eclipse NeoSCADA.
Signed-off-by: Jens Reimann <jreim...@redhat.com> Project: http://git-wip-us.apache.org/repos/asf/camel/repo Commit: http://git-wip-us.apache.org/repos/asf/camel/commit/eb4f6059 Tree: http://git-wip-us.apache.org/repos/asf/camel/tree/eb4f6059 Diff: http://git-wip-us.apache.org/repos/asf/camel/diff/eb4f6059 Branch: refs/heads/master Commit: eb4f6059cdfa5e363afdc57d258ea51775a59b88 Parents: 239fde8 Author: Jens Reimann <jreim...@redhat.com> Authored: Wed Aug 9 14:32:26 2017 +0200 Committer: Andrea Cosentino <anco...@gmail.com> Committed: Thu Aug 10 12:20:18 2017 +0200 ---------------------------------------------------------------------- components/camel-iec60870/pom.xml | 146 ++++++++++++ .../main/docs/iec60870-client-component.adoc | 127 +++++++++++ .../main/docs/iec60870-server-component.adoc | 111 +++++++++ .../iec60870/AbstractConnectionMultiplexor.java | 88 ++++++++ .../iec60870/AbstractIecComponent.java | 174 +++++++++++++++ .../component/iec60870/AbstractIecEndpoint.java | 109 +++++++++ .../camel/component/iec60870/BaseOptions.java | 223 +++++++++++++++++++ .../camel/component/iec60870/ConnectionId.java | 94 ++++++++ .../camel/component/iec60870/Constants.java | 25 +++ .../iec60870/DiscardAckChannelHandler.java | 54 +++++ .../component/iec60870/DiscardAckModule.java | 44 ++++ .../camel/component/iec60870/ObjectAddress.java | 114 ++++++++++ .../iec60870/client/ClientComponent.java | 78 +++++++ .../iec60870/client/ClientConnection.java | 132 +++++++++++ .../client/ClientConnectionMultiplexor.java | 43 ++++ .../iec60870/client/ClientConsumer.java | 79 +++++++ .../iec60870/client/ClientEndpoint.java | 46 ++++ .../iec60870/client/ClientOptions.java | 91 ++++++++ .../iec60870/client/ClientProducer.java | 89 ++++++++ .../iec60870/server/ServerComponent.java | 83 +++++++ .../server/ServerConnectionMultiplexor.java | 43 ++++ .../iec60870/server/ServerConsumer.java | 110 +++++++++ .../iec60870/server/ServerEndpoint.java | 60 +++++ .../iec60870/server/ServerInstance.java | 190 ++++++++++++++++ .../iec60870/server/ServerOptions.java | 152 +++++++++++++ .../iec60870/server/ServerProducer.java | 68 ++++++ .../src/main/resources/META-INF/LICENSE.txt | 203 +++++++++++++++++ .../src/main/resources/META-INF/NOTICE.txt | 11 + .../org/apache/camel/component/iec60870-client | 18 ++ .../org/apache/camel/component/iec60870-server | 18 ++ .../component/iec60870/ConnectionIdTest.java | 71 ++++++ .../component/iec60870/ConnectionTest.java | 171 ++++++++++++++ .../apache/camel/component/iec60870/Ports.java | 32 +++ .../iec60870/testing/ExampleApplication1.java | 76 +++++++ .../src/test/resources/log4j2.properties | 28 +++ components/readme.adoc | 4 +- parent/pom.xml | 6 + .../camel-iec60870-starter/pom.xml | 53 +++++ .../ClientComponentAutoConfiguration.java | 128 +++++++++++ .../ClientComponentConfiguration.java | 87 ++++++++ .../ServerComponentAutoConfiguration.java | 128 +++++++++++ .../ServerComponentConfiguration.java | 136 +++++++++++ .../src/main/resources/META-INF/LICENSE.txt | 203 +++++++++++++++++ .../src/main/resources/META-INF/NOTICE.txt | 11 + .../main/resources/META-INF/spring.factories | 21 ++ .../src/main/resources/META-INF/spring.provides | 17 ++ .../spring-boot/components-starter/pom.xml | 1 + 47 files changed, 3995 insertions(+), 1 deletion(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/camel/blob/eb4f6059/components/camel-iec60870/pom.xml ---------------------------------------------------------------------- diff --git a/components/camel-iec60870/pom.xml b/components/camel-iec60870/pom.xml new file mode 100644 index 0000000..6102479 --- /dev/null +++ b/components/camel-iec60870/pom.xml @@ -0,0 +1,146 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (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.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.apache.camel</groupId> + <artifactId>components</artifactId> + <version>2.20.0-SNAPSHOT</version> + </parent> + + <artifactId>camel-iec60870</artifactId> + <packaging>jar</packaging> + <name>Camel :: IEC 60870</name> + <description>Camel IEC 60870-5-104 support</description> + + <properties> + <camel.osgi.export.pkg> + !*.internal.*, + org.apache.camel.component.iec60870.* + </camel.osgi.export.pkg> + <camel.osgi.export.service> + org.apache.camel.spi.ComponentResolver;component=iec60870-client, + org.apache.camel.spi.ComponentResolver;component=iec60870-server + </camel.osgi.export.service> + </properties> + + <dependencies> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-core</artifactId> + </dependency> + + <dependency> + <groupId>org.eclipse.neoscada.protocols</groupId> + <artifactId>org.eclipse.neoscada.protocol.iec60870</artifactId> + <version>${neoscada-version}</version> + </dependency> + + <dependency> + <groupId>org.eclipse.neoscada.protocols</groupId> + <artifactId>org.eclipse.neoscada.protocol.iec60870.client</artifactId> + <version>${neoscada-version}</version> + </dependency> + + <dependency> + <groupId>org.eclipse.neoscada.protocols</groupId> + <artifactId>org.eclipse.neoscada.protocol.iec60870.client.data</artifactId> + <version>${neoscada-version}</version> + </dependency> + + <dependency> + <groupId>org.eclipse.neoscada.protocols</groupId> + <artifactId>org.eclipse.neoscada.protocol.iec60870.server</artifactId> + <version>${neoscada-version}</version> + </dependency> + + <dependency> + <groupId>org.eclipse.neoscada.protocols</groupId> + <artifactId>org.eclipse.neoscada.protocol.iec60870.server.data</artifactId> + <version>${neoscada-version}</version> + </dependency> + + <dependency> + <groupId>org.eclipse.neoscada.utils</groupId> + <artifactId>org.eclipse.scada.utils</artifactId> + <version>${neoscada-version}</version> + </dependency> + + <dependency> + <groupId>io.netty</groupId> + <artifactId>netty-buffer</artifactId> + <version>${netty-version}</version> + </dependency> + + <dependency> + <groupId>io.netty</groupId> + <artifactId>netty-codec</artifactId> + <version>${netty-version}</version> + </dependency> + + <dependency> + <groupId>io.netty</groupId> + <artifactId>netty-common</artifactId> + <version>${netty-version}</version> + </dependency> + + <dependency> + <groupId>io.netty</groupId> + <artifactId>netty-handler</artifactId> + <version>${netty-version}</version> + </dependency> + + <dependency> + <groupId>io.netty</groupId> + <artifactId>netty-transport</artifactId> + <version>${netty-version}</version> + </dependency> + + <!-- testing --> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-test</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.logging.log4j</groupId> + <artifactId>log4j-api</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.logging.log4j</groupId> + <artifactId>log4j-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.logging.log4j</groupId> + <artifactId>log4j-slf4j-impl</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + +</project> http://git-wip-us.apache.org/repos/asf/camel/blob/eb4f6059/components/camel-iec60870/src/main/docs/iec60870-client-component.adoc ---------------------------------------------------------------------- diff --git a/components/camel-iec60870/src/main/docs/iec60870-client-component.adoc b/components/camel-iec60870/src/main/docs/iec60870-client-component.adoc new file mode 100644 index 0000000..60367ec --- /dev/null +++ b/components/camel-iec60870/src/main/docs/iec60870-client-component.adoc @@ -0,0 +1,127 @@ +## IEC 60870 Client Component + +*Available as of Camel version 2.20* + +### IEC 60870-5-104 Client Component + +*Available as of Camel 2.20* + +The *IEC 60870-5-104 Client* component provides access to IEC 60870 servers using the +http://eclipse.org/eclipsescada[Eclipse NeoSCADAâ¢] implementation. + +*Java 8*: This component requires Java 8 at runtime. + +Maven users will need to add the following dependency to their `pom.xml` +for this component: + +[source,xml] +------------------------------------------------------------ +<dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-iec60870</artifactId> + <version>x.x.x</version> + <!-- use the same version as your Camel core version --> +</dependency> +------------------------------------------------------------ + +// component options: START +The IEC 60870 Client component supports 2 options which are listed below. + + + +[width="100%",cols="2,5,^1,2",options="header"] +|======================================================================= +| Name | Description | Default | Type +| **defaultConnection Options** (common) | Default connection options | | ClientOptions +| **resolveProperty Placeholders** (advanced) | Whether the component should resolve property placeholders on itself when starting. Only properties which are of String type can use property placeholders. | true | boolean +|======================================================================= +// component options: END + + + + + +### URI format + +The URI syntax of the endpoint is: + +[source] +------------------------ +iec60870-client:host:port/00-01-02-03-04 +------------------------ + +The information object address is encoded in the path in the syntax shows above. Please +note that always the full, 5 octet address format is being used. Unused octets have to be filled +with zero. + +### URI options + + +// endpoint options: START +The IEC 60870 Client endpoint is configured using URI syntax: + + iec60870-client:endpointUri + +with the following path and query parameters: + +#### Path Parameters (1 parameters): + +[width="100%",cols="2,5,^1,2",options="header"] +|======================================================================= +| Name | Description | Default | Type +| **uriPath** | *Required* The object information address | | ObjectAddress +|======================================================================= + +#### Query Parameters (18 parameters): + +[width="100%",cols="2,5,^1,2",options="header"] +|======================================================================= +| Name | Description | Default | Type +| **dataModuleOptions** (common) | Data module options | | DataModuleOptions +| **protocolOptions** (common) | Protocol options | | ProtocolOptions +| **bridgeErrorHandler** (consumer) | Allows for bridging the consumer to the Camel routing Error Handler which mean any exceptions occurred while the consumer is trying to pickup incoming messages or the likes will now be processed as a message and handled by the routing Error Handler. By default the consumer will use the org.apache.camel.spi.ExceptionHandler to deal with exceptions that will be logged at WARN or ERROR level and ignored. | false | boolean +| **exceptionHandler** (consumer) | To let the consumer use a custom ExceptionHandler. Notice if the option bridgeErrorHandler is enabled then this options is not in use. By default the consumer will deal with exceptions that will be logged at WARN or ERROR level and ignored. | | ExceptionHandler +| **exchangePattern** (consumer) | Sets the exchange pattern when the consumer creates an exchange. | | ExchangePattern +| **synchronous** (advanced) | Sets whether synchronous processing should be strictly used or Camel is allowed to use asynchronous processing (if supported). | false | boolean +| **acknowledgeWindow** (connection) | Parameter W - Acknowledgment window. | 10 | short +| **adsuAddressType** (connection) | The common ASDU address size. May be either SIZE_1 or SIZE_2. | | ASDUAddressType +| **causeOfTransmissionType** (connection) | The cause of transmission type. May be either SIZE_1 or SIZE_2. | | CauseOfTransmission Type +| **informationObjectAddress Type** (connection) | The information address size. May be either SIZE_1 SIZE_2 or SIZE_3. | | InformationObject AddressType +| **maxUnacknowledged** (connection) | Parameter K - Maximum number of un-acknowledged messages. | 15 | short +| **timeout1** (connection) | Timeout T1 in milliseconds. | 15000 | int +| **timeout2** (connection) | Timeout T2 in milliseconds. | 10000 | int +| **timeout3** (connection) | Timeout T3 in milliseconds. | 20000 | int +| **ignoreBackgroundScan** (data) | Whether background scan transmissions should be ignored. | true | boolean +| **ignoreDaylightSavingTime** (data) | Whether to ignore or respect DST | false | boolean +| **timeZone** (data) | The timezone to use. May be any Java time zone string | UTC | TimeZone +| **connectionId** (id) | An identifier grouping connection instances | | String +|======================================================================= +// endpoint options: END + + + +A connection instance if identified by the host and port part of the URI, plus all parameters in the "id" group. +If a new connection id is encountered the connection options will be evaluated and the connection instance +is created with those options. + + +NOTE: If two URIs specify the same connection (host, port, â¦) but different connection options, then it is +undefined which of those connection options will be used. + + +The final connection options will be evaluated in the following order: + +* If present, the +connectionOptions+ parameter will be used +* Otherwise the +defaultConnectionOptions+ instance is copied and customized in the following steps +* Apply +protocolOptions+ if present +* Apply +dataModuleOptions+ if present +* Apply all explicit connection parameters (e.g. +timeZone+) + + + +### See Also + +* link:configuring-camel.html[Configuring Camel] +* link:component.html[Component] +* link:endpoint.html[Endpoint] +* link:getting-started.html[Getting Started] http://git-wip-us.apache.org/repos/asf/camel/blob/eb4f6059/components/camel-iec60870/src/main/docs/iec60870-server-component.adoc ---------------------------------------------------------------------- diff --git a/components/camel-iec60870/src/main/docs/iec60870-server-component.adoc b/components/camel-iec60870/src/main/docs/iec60870-server-component.adoc new file mode 100644 index 0000000..338e8d7 --- /dev/null +++ b/components/camel-iec60870/src/main/docs/iec60870-server-component.adoc @@ -0,0 +1,111 @@ +## IEC 60870-5-104 server Component + +*Available as of Camel version 2.20* + +### IEC 60870-5-104 Server Component + +*Available as of Camel 2.20* + +The *IEC 60870-5-104 Server* component provides access to IEC 60870 servers using the +http://eclipse.org/eclipsescada[Eclipse NeoSCADAâ¢] implementation. + +*Java 8*: This component requires Java 8 at runtime. + +Maven users will need to add the following dependency to their `pom.xml` +for this component: + +[source,xml] +------------------------------------------------------------ +<dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-iec60870</artifactId> + <version>x.x.x</version> + <!-- use the same version as your Camel core version --> +</dependency> +------------------------------------------------------------ + + +// component options: START +The IEC 60870-5-104 server component supports 2 options which are listed below. + + + +[width="100%",cols="2,5,^1,2",options="header"] +|======================================================================= +| Name | Description | Default | Type +| **defaultConnection Options** (common) | Default connection options | | ServerOptions +| **resolveProperty Placeholders** (advanced) | Whether the component should resolve property placeholders on itself when starting. Only properties which are of String type can use property placeholders. | true | boolean +|======================================================================= +// component options: END + + + + + +### URI format + +The URI syntax of the endpoint is: + +[source] +------------------------ +iec60870-server:host:port/00-01-02-03-04 +------------------------ + +The information object address is encoded in the path in the syntax shows above. Please +note that always the full, 5 octet address format is being used. Unused octets have to be filled +with zero. + +### URI options + + + + +// endpoint options: START +The IEC 60870-5-104 server endpoint is configured using URI syntax: + + iec60870-server:endpointUri + +with the following path and query parameters: + +#### Path Parameters (1 parameters): + +[width="100%",cols="2,5,^1,2",options="header"] +|======================================================================= +| Name | Description | Default | Type +| **uriPath** | *Required* The object information address | | ObjectAddress +|======================================================================= + +#### Query Parameters (19 parameters): + +[width="100%",cols="2,5,^1,2",options="header"] +|======================================================================= +| Name | Description | Default | Type +| **dataModuleOptions** (common) | Data module options | | DataModuleOptions +| **filterNonExecute** (common) | Filter out all requests which don't have the execute bit set | true | boolean +| **protocolOptions** (common) | Protocol options | | ProtocolOptions +| **bridgeErrorHandler** (consumer) | Allows for bridging the consumer to the Camel routing Error Handler which mean any exceptions occurred while the consumer is trying to pickup incoming messages or the likes will now be processed as a message and handled by the routing Error Handler. By default the consumer will use the org.apache.camel.spi.ExceptionHandler to deal with exceptions that will be logged at WARN or ERROR level and ignored. | false | boolean +| **exceptionHandler** (consumer) | To let the consumer use a custom ExceptionHandler. Notice if the option bridgeErrorHandler is enabled then this options is not in use. By default the consumer will deal with exceptions that will be logged at WARN or ERROR level and ignored. | | ExceptionHandler +| **exchangePattern** (consumer) | Sets the exchange pattern when the consumer creates an exchange. | | ExchangePattern +| **synchronous** (advanced) | Sets whether synchronous processing should be strictly used or Camel is allowed to use asynchronous processing (if supported). | false | boolean +| **acknowledgeWindow** (connection) | Parameter W - Acknowledgment window. | 10 | short +| **adsuAddressType** (connection) | The common ASDU address size. May be either SIZE_1 or SIZE_2. | | ASDUAddressType +| **causeOfTransmissionType** (connection) | The cause of transmission type. May be either SIZE_1 or SIZE_2. | | CauseOfTransmission Type +| **informationObjectAddress Type** (connection) | The information address size. May be either SIZE_1 SIZE_2 or SIZE_3. | | InformationObject AddressType +| **maxUnacknowledged** (connection) | Parameter K - Maximum number of un-acknowledged messages. | 15 | short +| **timeout1** (connection) | Timeout T1 in milliseconds. | 15000 | int +| **timeout2** (connection) | Timeout T2 in milliseconds. | 10000 | int +| **timeout3** (connection) | Timeout T3 in milliseconds. | 20000 | int +| **ignoreBackgroundScan** (data) | Whether background scan transmissions should be ignored. | true | boolean +| **ignoreDaylightSavingTime** (data) | Whether to ignore or respect DST | false | boolean +| **timeZone** (data) | The timezone to use. May be any Java time zone string | UTC | TimeZone +| **connectionId** (id) | An identifier grouping connection instances | | String +|======================================================================= +// endpoint options: END + + +### See Also + +* link:configuring-camel.html[Configuring Camel] +* link:component.html[Component] +* link:endpoint.html[Endpoint] +* link:getting-started.html[Getting Started] http://git-wip-us.apache.org/repos/asf/camel/blob/eb4f6059/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/AbstractConnectionMultiplexor.java ---------------------------------------------------------------------- diff --git a/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/AbstractConnectionMultiplexor.java b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/AbstractConnectionMultiplexor.java new file mode 100644 index 0000000..18fa1bf --- /dev/null +++ b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/AbstractConnectionMultiplexor.java @@ -0,0 +1,88 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.iec60870; + +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class AbstractConnectionMultiplexor { + + private static final Logger LOG = LoggerFactory.getLogger(AbstractConnectionMultiplexor.class); + + public interface Handle { + void unregister() throws Exception; + } + + private final class HandleImplementation implements Handle { + @Override + public void unregister() throws Exception { + AbstractConnectionMultiplexor.this.unregister(this); + } + } + + private final Set<HandleImplementation> handles = new CopyOnWriteArraySet<>(); + + public synchronized Handle register() throws Exception { + final HandleImplementation handle = new HandleImplementation(); + + final boolean needStart = this.handles.isEmpty(); + this.handles.add(handle); + + if (needStart) { + LOG.info("Calling performStart()"); + performStart(); + } + + return handle; + } + + private synchronized void unregister(final HandleImplementation handle) throws Exception { + if (!this.handles.remove(handle)) { + return; + } + + if (this.handles.isEmpty()) { + LOG.info("Calling performStop()"); + performStop(); + } + } + + public synchronized void dispose() { + + LOG.info("Disposing"); + if (this.handles.isEmpty()) { + LOG.debug("Disposing - not started"); + return; + } + + LOG.debug("Disposing - calling performStop()"); + + this.handles.clear(); + try { + performStop(); + } catch (final Exception e) { + throw new RuntimeException("Failed to stop on dispose", e); + } + } + + protected abstract void performStart() throws Exception; + + protected abstract void performStop() throws Exception; +} http://git-wip-us.apache.org/repos/asf/camel/blob/eb4f6059/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/AbstractIecComponent.java ---------------------------------------------------------------------- diff --git a/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/AbstractIecComponent.java b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/AbstractIecComponent.java new file mode 100644 index 0000000..93ca36c --- /dev/null +++ b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/AbstractIecComponent.java @@ -0,0 +1,174 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.iec60870; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; + +import static java.util.Objects.requireNonNull; + +import org.apache.camel.CamelContext; +import org.apache.camel.Endpoint; +import org.apache.camel.component.iec60870.client.ClientOptions; +import org.apache.camel.impl.DefaultComponent; +import org.eclipse.neoscada.protocol.iec60870.ProtocolOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class AbstractIecComponent<T1, T2 extends BaseOptions<T2>> extends DefaultComponent { + + private static final Logger LOG = LoggerFactory.getLogger(AbstractIecComponent.class); + + private final Map<ConnectionId, T1> connections = new HashMap<>(); + + private final Class<T2> connectionOptionsClazz; + + private T2 defaultConnectionOptions; + + public AbstractIecComponent(final Class<T2> connectionOptionsClazz, final T2 defaultConnectionOptions, final Class<? extends Endpoint> endpointClass) { + this.connectionOptionsClazz = connectionOptionsClazz; + this.defaultConnectionOptions = defaultConnectionOptions; + } + + public AbstractIecComponent(final Class<T2> connectionOptionsClazz, final T2 defaultConnectionOptions, final CamelContext context, + final Class<? extends Endpoint> endpointClass) { + super(context); + this.connectionOptionsClazz = connectionOptionsClazz; + this.defaultConnectionOptions = defaultConnectionOptions; + } + + protected abstract T1 createConnection(ConnectionId id, T2 options); + + /** + * Default connection options + * + * @param defaultConnectionOptions the new default connection options, must + * not be {@code null} + */ + protected void setDefaultConnectionOptions(final T2 defaultConnectionOptions) { + this.defaultConnectionOptions = requireNonNull(defaultConnectionOptions); + } + + /** + * Get the default connection options + * + * @return the default connect options, never returns {@code null} + */ + protected T2 getDefaultConnectionOptions() { + return this.defaultConnectionOptions; + } + + @Override + protected Endpoint createEndpoint(final String uri, final String remaining, final Map<String, Object> parameters) throws Exception { + + LOG.info("Create endpoint - uri: {}, remaining: {}, parameters: {}", uri, remaining, parameters); + + final T1 connection = lookupConnection(uri, parameters); + final ObjectAddress address = parseAddress(uri); + + return createEndpoint(uri, connection, address); + } + + protected abstract Endpoint createEndpoint(String uri, T1 connection, ObjectAddress address); + + protected T2 parseOptions(final ConnectionId id, final Map<String, Object> parameters) throws Exception { + + // test for provided connection options + + final Object connectionOptions = parameters.get(Constants.PARAM_CONNECTION_OPTIONS); + if (connectionOptions != null) { + try { + return this.connectionOptionsClazz.cast(connectionOptions); + } catch (final ClassCastException e) { + throw new IllegalArgumentException(String.format("'%s' must by of type %s", Constants.PARAM_CONNECTION_OPTIONS, ClientOptions.class.getName()), e); + } + } + + // construct new default set + + final T2 options = this.defaultConnectionOptions.copy(); + + // apply protocolOptions + + if (parameters.get(Constants.PARAM_PROTOCOL_OPTIONS) instanceof ProtocolOptions) { + options.setProtocolOptions((ProtocolOptions)parameters.get(Constants.PARAM_PROTOCOL_OPTIONS)); + } + + // apply dataModuleOptions + + applyDataModuleOptions(options, parameters); + + // apply parameters to connection options + + setProperties(options, parameters); + + // return result + + return options; + } + + protected abstract void applyDataModuleOptions(T2 options, Map<String, Object> parameters); + + private T1 lookupConnection(final String fullUri, final Map<String, Object> parameters) throws Exception { + + LOG.debug("parse connection - '{}'", fullUri); + + if (fullUri == null || fullUri.isEmpty()) { + throw new IllegalArgumentException("Invalid URI: " + fullUri); + } + + final ConnectionId id = parseConnectionId(fullUri, parameters); + + LOG.debug("parse connection - fullUri: {} -> {}", fullUri, id); + + synchronized (this) { + LOG.debug("Locating connection - {}", id); + + T1 connection = this.connections.get(id); + + LOG.debug("Result - {} -> {}", id, connection); + + if (connection == null) { + final T2 options = parseOptions(id, parameters); + LOG.debug("Creating new connection: {}", options); + + connection = createConnection(id, options); + this.connections.put(id, connection); + } + return connection; + } + } + + private static ConnectionId parseConnectionId(final String fullUri, final Map<String, Object> parameters) { + final URI uri = URI.create(fullUri); + + final Object connectionId = parameters.get("connectionId"); + + return new ConnectionId(uri.getHost(), uri.getPort(), connectionId instanceof String ? (String)connectionId : null); + } + + private static ObjectAddress parseAddress(final String fullUri) { + final URI uri = URI.create(fullUri); + + String path = uri.getPath(); + path = path.replaceAll("^\\/+", ""); + + return ObjectAddress.valueOf(path); + } + +} http://git-wip-us.apache.org/repos/asf/camel/blob/eb4f6059/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/AbstractIecEndpoint.java ---------------------------------------------------------------------- diff --git a/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/AbstractIecEndpoint.java b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/AbstractIecEndpoint.java new file mode 100644 index 0000000..98fd48c --- /dev/null +++ b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/AbstractIecEndpoint.java @@ -0,0 +1,109 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.iec60870; + +import java.util.concurrent.atomic.AtomicReference; + +import static java.util.Objects.requireNonNull; + +import org.apache.camel.component.iec60870.AbstractConnectionMultiplexor.Handle; +import org.apache.camel.component.iec60870.client.ClientOptions; +import org.apache.camel.impl.DefaultComponent; +import org.apache.camel.impl.DefaultEndpoint; +import org.apache.camel.spi.Metadata; +import org.apache.camel.spi.UriParam; +import org.apache.camel.spi.UriPath; +import org.eclipse.neoscada.protocol.iec60870.ProtocolOptions; +import org.eclipse.neoscada.protocol.iec60870.client.data.DataModuleOptions; + +public abstract class AbstractIecEndpoint<T extends AbstractConnectionMultiplexor> extends DefaultEndpoint { + + /** + * The object information address + */ + @UriPath(name = "uriPath") + @Metadata(required = "true") + private final ObjectAddress address; + + // dummy for doc generation + /** + * A full set of connection options + */ + @UriParam + private ClientOptions connectionOptions; + + // dummy for doc generation + /** + * A set of protocol options + */ + @UriParam + private ProtocolOptions protocolOptions; + + // dummy for doc generation + /** + * A set of data module options + */ + @UriParam + private DataModuleOptions dataModuleOptions; + + // dummy for doc generation + /** + * An identifier grouping connection instances + */ + @UriParam(label = "id") + private String connectionId; + + private final T connection; + + private final AtomicReference<Handle> connectionHandle = new AtomicReference<>(); + + public AbstractIecEndpoint(final String uri, final DefaultComponent component, final T connection, final ObjectAddress address) { + super(uri, component); + + this.connection = requireNonNull(connection); + this.address = requireNonNull(address); + } + + public ObjectAddress getAddress() { + return this.address; + } + + @Override + protected void doStart() throws Exception { + super.doStart(); + this.connectionHandle.set(this.connection.register()); + } + + @Override + protected void doStop() throws Exception { + final Handle connectionHandle = this.connectionHandle.getAndSet(null); + if (connectionHandle != null) { + connectionHandle.unregister(); + } + super.doStop(); + } + + protected T getConnection() { + return this.connection; + } + + @Override + public boolean isSingleton() { + return true; + } + +} http://git-wip-us.apache.org/repos/asf/camel/blob/eb4f6059/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/BaseOptions.java ---------------------------------------------------------------------- diff --git a/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/BaseOptions.java b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/BaseOptions.java new file mode 100644 index 0000000..e7e63c3 --- /dev/null +++ b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/BaseOptions.java @@ -0,0 +1,223 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.iec60870; + +import java.util.Objects; +import java.util.TimeZone; + +import org.apache.camel.spi.UriParam; +import org.apache.camel.spi.UriParams; +import org.eclipse.neoscada.protocol.iec60870.ASDUAddressType; +import org.eclipse.neoscada.protocol.iec60870.CauseOfTransmissionType; +import org.eclipse.neoscada.protocol.iec60870.InformationObjectAddressType; +import org.eclipse.neoscada.protocol.iec60870.ProtocolOptions; + +@UriParams +public abstract class BaseOptions<T extends BaseOptions<T>> { + + /** + * Protocol options + */ + @UriParam(javaType = "ProtocolOptions") + private ProtocolOptions.Builder protocolOptions; + + // dummy for doc generation + /** + * The common ASDU address size. + * <p> + * May be either {@code SIZE_1} or {@code SIZE_2}. + * </p> + */ + @UriParam(enums = "SIZE_1, SIZE_2", label = "connection") + private ASDUAddressType adsuAddressType; + + // dummy for doc generation + /** + * The information address size. + * <p> + * May be either {@code SIZE_1}, {@code SIZE_2} or {@code SIZE_3}. + * </p> + */ + @UriParam(enums = "SIZE_1, SIZE_2, SIZE_3", label = "connection") + private InformationObjectAddressType informationObjectAddressType; + + // dummy for doc generation + /** + * The cause of transmission type. + * <p> + * May be either {@code SIZE_1} or {@code SIZE_2}. + * </p> + */ + @UriParam(enums = "SIZE_1, SIZE_2", label = "connection") + private CauseOfTransmissionType causeOfTransmissionType; + + // dummy for doc generation + /** + * The timezone to use. + * <p> + * May be any Java time zone string + * </p> + */ + @UriParam(label = "data", defaultValue = "UTC") + private TimeZone timeZone; + + // dummy for doc generation + /** + * Whether to ignore or respect DST + */ + @UriParam(label = "data") + private boolean ignoreDaylightSavingTime; + + // dummy for doc generation + /** + * Timeout T1 in milliseconds. + */ + @UriParam(label = "connection", defaultValue = "15000") + private int timeout1; + + // dummy for doc generation + /** + * Timeout T2 in milliseconds. + */ + @UriParam(label = "connection", defaultValue = "10000") + private int timeout2; + + // dummy for doc generation + /** + * Timeout T3 in milliseconds. + */ + @UriParam(label = "connection", defaultValue = "20000") + private int timeout3; + + // dummy for doc generation + /** + * Parameter "K" - Maximum number of un-acknowledged messages. + */ + @UriParam(label = "connection", defaultValue = "15") + private short maxUnacknowledged; + + // dummy for doc generation + /** + * Parameter "W" - Acknowledgment window. + */ + @UriParam(label = "connection", defaultValue = "10") + private short acknowledgeWindow; + + public BaseOptions() { + this.protocolOptions = new ProtocolOptions.Builder(); + } + + public BaseOptions(final ProtocolOptions protocolOptions) { + Objects.requireNonNull(protocolOptions); + this.protocolOptions = new ProtocolOptions.Builder(protocolOptions); + } + + public void setProtocolOptions(final ProtocolOptions protocolOptions) { + Objects.requireNonNull(protocolOptions); + + this.protocolOptions = new ProtocolOptions.Builder(protocolOptions); + } + + public ProtocolOptions getProtocolOptions() { + return this.protocolOptions.build(); + } + + public abstract T copy(); + + // wrapper methods - ProtocolOptions + + public int getTimeout1() { + return this.protocolOptions.getTimeout1(); + } + + public void setTimeout1(final int timeout1) { + this.protocolOptions.setTimeout1(timeout1); + } + + public int getTimeout2() { + return this.protocolOptions.getTimeout2(); + } + + public void setTimeout2(final int timeout2) { + this.protocolOptions.setTimeout2(timeout2); + } + + public int getTimeout3() { + return this.protocolOptions.getTimeout3(); + } + + public void setTimeout3(final int timeout3) { + this.protocolOptions.setTimeout3(timeout3); + } + + public short getAcknowledgeWindow() { + return this.protocolOptions.getAcknowledgeWindow(); + } + + public void setAcknowledgeWindow(final short acknowledgeWindow) { + this.protocolOptions.setAcknowledgeWindow(acknowledgeWindow); + } + + public short getMaxUnacknowledged() { + return this.protocolOptions.getMaxUnacknowledged(); + } + + public void setMaxUnacknowledged(final short maxUnacknowledged) { + this.protocolOptions.setMaxUnacknowledged(maxUnacknowledged); + } + + public ASDUAddressType getAdsuAddressType() { + return this.protocolOptions.getAdsuAddressType(); + } + + public void setAdsuAddressType(final ASDUAddressType adsuAddressType) { + this.protocolOptions.setAdsuAddressType(adsuAddressType); + } + + public InformationObjectAddressType getInformationObjectAddressType() { + return this.protocolOptions.getInformationObjectAddressType(); + } + + public void setInformationObjectAddressType(final InformationObjectAddressType informationObjectAddressType) { + this.protocolOptions.setInformationObjectAddressType(informationObjectAddressType); + } + + public CauseOfTransmissionType getCauseOfTransmissionType() { + return this.protocolOptions.getCauseOfTransmissionType(); + } + + public void setCauseOfTransmissionType(final CauseOfTransmissionType causeOfTransmissionType) { + this.protocolOptions.setCauseOfTransmissionType(causeOfTransmissionType); + } + + public TimeZone getTimeZone() { + return this.protocolOptions.getTimeZone(); + } + + public void setTimeZone(final TimeZone timeZone) { + this.protocolOptions.setTimeZone(timeZone); + } + + public void setIgnoreDaylightSavingTime(final boolean ignoreDaylightSavingTime) { + this.protocolOptions.setIgnoreDaylightSavingTime(ignoreDaylightSavingTime); + } + + public boolean isIgnoreDaylightSavingTime() { + return this.protocolOptions.isIgnoreDaylightSavingTime(); + } + +} http://git-wip-us.apache.org/repos/asf/camel/blob/eb4f6059/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/ConnectionId.java ---------------------------------------------------------------------- diff --git a/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/ConnectionId.java b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/ConnectionId.java new file mode 100644 index 0000000..66dfc07 --- /dev/null +++ b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/ConnectionId.java @@ -0,0 +1,94 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.iec60870; + +import java.util.Objects; + +public class ConnectionId { + private final String host; + + private final int port; + + private final String connectionId; + + public ConnectionId(final String host, final int port, final String connectionId) { + Objects.requireNonNull(host); + + if (port <= 0) { + throw new IllegalArgumentException(String.format("Port must be greater than 0")); + } + + this.host = host; + this.port = port; + this.connectionId = connectionId; + } + + public String getHost() { + return this.host; + } + + public int getPort() { + return this.port; + } + + public String getConnectionId() { + return this.connectionId; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (this.connectionId == null ? 0 : this.connectionId.hashCode()); + result = prime * result + (this.host == null ? 0 : this.host.hashCode()); + result = prime * result + this.port; + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ConnectionId other = (ConnectionId)obj; + if (this.connectionId == null) { + if (other.connectionId != null) { + return false; + } + } else if (!this.connectionId.equals(other.connectionId)) { + return false; + } + if (this.host == null) { + if (other.host != null) { + return false; + } + } else if (!this.host.equals(other.host)) { + return false; + } + if (this.port != other.port) { + return false; + } + return true; + } + +} http://git-wip-us.apache.org/repos/asf/camel/blob/eb4f6059/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/Constants.java ---------------------------------------------------------------------- diff --git a/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/Constants.java b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/Constants.java new file mode 100644 index 0000000..f2f5a60 --- /dev/null +++ b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/Constants.java @@ -0,0 +1,25 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.iec60870; + +public interface Constants { + String PARAM_DATA_MODULE_OPTIONS = "dataModuleOptions"; + + String PARAM_PROTOCOL_OPTIONS = "protocolOptions"; + + String PARAM_CONNECTION_OPTIONS = "connectionOptions"; +} http://git-wip-us.apache.org/repos/asf/camel/blob/eb4f6059/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/DiscardAckChannelHandler.java ---------------------------------------------------------------------- diff --git a/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/DiscardAckChannelHandler.java b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/DiscardAckChannelHandler.java new file mode 100644 index 0000000..32cbd69 --- /dev/null +++ b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/DiscardAckChannelHandler.java @@ -0,0 +1,54 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.iec60870; + +import java.util.HashSet; +import java.util.Set; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import org.eclipse.neoscada.protocol.iec60870.asdu.message.AbstractMessage; +import org.eclipse.neoscada.protocol.iec60870.asdu.types.Cause; +import org.eclipse.neoscada.protocol.iec60870.asdu.types.StandardCause; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DiscardAckChannelHandler extends ChannelInboundHandlerAdapter { + + private static final Logger LOG = LoggerFactory.getLogger(DiscardAckChannelHandler.class); + + private final Set<Cause> discards = new HashSet<>(); + + public DiscardAckChannelHandler() { + this.discards.add(StandardCause.ACTIVATION_CONFIRM); + this.discards.add(StandardCause.ACTIVATION_TERMINATION); + this.discards.add(StandardCause.DEACTIVATION_CONFIRM); + } + + @Override + public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception { + if (msg instanceof AbstractMessage) { + final AbstractMessage amsg = (AbstractMessage)msg; + final Cause cause = amsg.getHeader().getCauseOfTransmission().getCause(); + if (this.discards.contains(cause)) { + LOG.debug("Discarding: {}", cause); + return; + } + } + super.channelRead(ctx, msg); + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/eb4f6059/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/DiscardAckModule.java ---------------------------------------------------------------------- diff --git a/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/DiscardAckModule.java b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/DiscardAckModule.java new file mode 100644 index 0000000..4fb9c6d --- /dev/null +++ b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/DiscardAckModule.java @@ -0,0 +1,44 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.iec60870; + +import io.netty.channel.socket.SocketChannel; +import org.eclipse.neoscada.protocol.iec60870.apci.MessageChannel; +import org.eclipse.neoscada.protocol.iec60870.asdu.MessageManager; +import org.eclipse.neoscada.protocol.iec60870.client.Client; +import org.eclipse.neoscada.protocol.iec60870.client.ClientModule; +import org.eclipse.neoscada.protocol.iec60870.server.Server; +import org.eclipse.neoscada.protocol.iec60870.server.ServerModule; + +public class DiscardAckModule implements ClientModule, ServerModule { + @Override + public void initializeChannel(final SocketChannel channel, final MessageChannel messageChannel) { + channel.pipeline().addLast(new DiscardAckChannelHandler()); + } + + @Override + public void dispose() { + } + + @Override + public void initializeClient(final Client client, final MessageManager manager) { + } + + @Override + public void initializeServer(final Server server, final MessageManager manager) { + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/eb4f6059/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/ObjectAddress.java ---------------------------------------------------------------------- diff --git a/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/ObjectAddress.java b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/ObjectAddress.java new file mode 100644 index 0000000..232a506 --- /dev/null +++ b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/ObjectAddress.java @@ -0,0 +1,114 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.iec60870; + +import java.util.Arrays; +import java.util.Objects; + +import org.eclipse.neoscada.protocol.iec60870.asdu.types.ASDUAddress; +import org.eclipse.neoscada.protocol.iec60870.asdu.types.InformationObjectAddress; + +public class ObjectAddress { + int[] address; + + private ObjectAddress(final int[] address) { + this.address = address; + } + + public ObjectAddress(final int a1, final int a2, final int a3, final int a4, final int a5) { + this.address = new int[] {a1, a2, a3, a4, a5}; + } + + @Override + public String toString() { + return String.format("%02d-%02d-%02d-%02d-%02d", this.address[0], this.address[1], this.address[2], this.address[3], this.address[4]); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(this.address); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ObjectAddress other = (ObjectAddress)obj; + if (!Arrays.equals(this.address, other.address)) { + return false; + } + return true; + } + + public static ObjectAddress valueOf(final ASDUAddress asduAddress, final InformationObjectAddress address) { + Objects.requireNonNull(asduAddress); + Objects.requireNonNull(address); + + final int[] a = asduAddress.toArray(); + final int[] b = address.toArray(); + + return new ObjectAddress(a[0], a[1], b[0], b[1], b[2]); + } + + public static ObjectAddress valueOf(final String address) { + if (address == null || address.isEmpty()) { + return null; + } + + final String[] toks = address.split("-"); + if (toks.length != 5) { + throw new IllegalArgumentException("Invalid address. Must have 5 octets."); + } + + final int[] a = new int[toks.length]; + + for (int i = 0; i < toks.length; i++) { + final int v; + try { + v = Integer.parseInt(toks[i]); + } catch (final NumberFormatException e) { + throw new IllegalArgumentException("Address segment must be numeric", e); + } + + if (v < 0 || v > 255) { + throw new IllegalArgumentException(String.format("Address segment must be an octet, between 0 and 255 (is %s)", v)); + } + + a[i] = v; + } + + return new ObjectAddress(a); + } + + public ASDUAddress getASDUAddress() { + return ASDUAddress.fromArray(new int[] {this.address[0], this.address[1]}); + } + + public InformationObjectAddress getInformationObjectAddress() { + return InformationObjectAddress.fromArray(new int[] {this.address[2], this.address[3], this.address[4]}); + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/eb4f6059/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/client/ClientComponent.java ---------------------------------------------------------------------- diff --git a/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/client/ClientComponent.java b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/client/ClientComponent.java new file mode 100644 index 0000000..1609ed1 --- /dev/null +++ b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/client/ClientComponent.java @@ -0,0 +1,78 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.iec60870.client; + +import java.util.Map; + +import org.apache.camel.CamelContext; +import org.apache.camel.Endpoint; +import org.apache.camel.component.iec60870.AbstractIecComponent; +import org.apache.camel.component.iec60870.ConnectionId; +import org.apache.camel.component.iec60870.Constants; +import org.apache.camel.component.iec60870.ObjectAddress; +import org.eclipse.neoscada.protocol.iec60870.client.data.DataModuleOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ClientComponent extends AbstractIecComponent<ClientConnectionMultiplexor, ClientOptions> { + + private static final Logger LOG = LoggerFactory.getLogger(ClientComponent.class); + + public ClientComponent(final CamelContext context) { + super(ClientOptions.class, new ClientOptions(), context, ClientEndpoint.class); + } + + public ClientComponent() { + super(ClientOptions.class, new ClientOptions(), ClientEndpoint.class); + } + + @Override + protected void applyDataModuleOptions(final ClientOptions options, final Map<String, Object> parameters) { + if (parameters.get(Constants.PARAM_DATA_MODULE_OPTIONS) instanceof DataModuleOptions) { + options.setDataModuleOptions((DataModuleOptions)parameters.get(Constants.PARAM_DATA_MODULE_OPTIONS)); + } + } + + @Override + protected Endpoint createEndpoint(final String uri, final ClientConnectionMultiplexor connection, final ObjectAddress address) { + return new ClientEndpoint(uri, this, connection, address); + } + + @Override + protected ClientConnectionMultiplexor createConnection(final ConnectionId id, final ClientOptions options) { + LOG.debug("Create new connection - id: {}", id); + + return new ClientConnectionMultiplexor(new ClientConnection(id.getHost(), id.getPort(), options)); + } + + /** + * Default connection options + * + * @param defaultConnectionOptions the new default connection options, must + * not be {@code null} + */ + @Override + public void setDefaultConnectionOptions(final ClientOptions defaultConnectionOptions) { + super.setDefaultConnectionOptions(defaultConnectionOptions); + } + + @Override + public ClientOptions getDefaultConnectionOptions() { + return super.getDefaultConnectionOptions(); + } + +} http://git-wip-us.apache.org/repos/asf/camel/blob/eb4f6059/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/client/ClientConnection.java ---------------------------------------------------------------------- diff --git a/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/client/ClientConnection.java b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/client/ClientConnection.java new file mode 100644 index 0000000..c4c47c7 --- /dev/null +++ b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/client/ClientConnection.java @@ -0,0 +1,132 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.iec60870.client; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import io.netty.channel.ChannelHandlerContext; +import org.apache.camel.component.iec60870.DiscardAckModule; +import org.apache.camel.component.iec60870.ObjectAddress; +import org.eclipse.neoscada.protocol.iec60870.asdu.types.ASDUAddress; +import org.eclipse.neoscada.protocol.iec60870.asdu.types.InformationObjectAddress; +import org.eclipse.neoscada.protocol.iec60870.asdu.types.QualifierOfInterrogation; +import org.eclipse.neoscada.protocol.iec60870.asdu.types.Value; +import org.eclipse.neoscada.protocol.iec60870.client.AutoConnectClient; +import org.eclipse.neoscada.protocol.iec60870.client.AutoConnectClient.ModulesFactory; +import org.eclipse.neoscada.protocol.iec60870.client.AutoConnectClient.State; +import org.eclipse.neoscada.protocol.iec60870.client.AutoConnectClient.StateListener; +import org.eclipse.neoscada.protocol.iec60870.client.data.AbstractDataProcessor; +import org.eclipse.neoscada.protocol.iec60870.client.data.DataHandler; +import org.eclipse.neoscada.protocol.iec60870.client.data.DataModule; +import org.eclipse.neoscada.protocol.iec60870.client.data.DataModuleContext; + +public class ClientConnection { + + @FunctionalInterface + public interface ValueListener { + void update(ObjectAddress address, Value<?> value); + } + + private final StateListener stateListener = new StateListener() { + + @Override + public void stateChanged(final State state, final Throwable e) { + } + }; + + private final DataHandler dataHandler = new AbstractDataProcessor() { + + /** + * Called when the connection was established + */ + @Override + public void activated(final DataModuleContext dataModuleContext, final ChannelHandlerContext ctx) { + dataModuleContext.requestStartData(); + dataModuleContext.startInterrogation(ASDUAddress.BROADCAST, QualifierOfInterrogation.GLOBAL); + } + + /** + * Called when the start data was accepted + */ + @Override + public void started() { + } + + /** + * Called when the connection broke + */ + @Override + public void disconnected() { + } + + @Override + protected void fireEntry(final ASDUAddress asduAddress, final InformationObjectAddress address, final Value<?> value) { + ClientConnection.this.handleData(ObjectAddress.valueOf(asduAddress, address), value); + } + }; + + private final Map<ObjectAddress, Value<?>> lastValue = new HashMap<>(); + private final Map<ObjectAddress, ValueListener> listeners = new HashMap<>(); + + private final String host; + private final int port; + private final ClientOptions options; + + private AutoConnectClient client; + + public ClientConnection(final String host, final int port, final ClientOptions options) { + this.host = host; + this.port = port; + this.options = options; + } + + public void start() { + final DataModule dataModule = new DataModule(this.dataHandler, this.options.getDataModuleOptions()); + final ModulesFactory factory = () -> Arrays.asList(dataModule, new DiscardAckModule()); + this.client = new AutoConnectClient(this.host, this.port, this.options.getProtocolOptions(), factory, this.stateListener); + } + + public void stop() throws Exception { + this.client.close(); + } + + protected synchronized void handleData(final ObjectAddress address, final Value<?> value) { + this.lastValue.put(address, value); + final ValueListener listener = this.listeners.get(address); + if (listener != null) { + listener.update(address, value); + } + } + + public synchronized void setListener(final ObjectAddress address, final ValueListener listener) { + if (listener != null) { + this.listeners.put(address, listener); + final Value<?> last = this.lastValue.get(address); + if (last != null) { + listener.update(address, last); + } + } else { + this.listeners.remove(address); + } + } + + public boolean executeCommand(final Object command) { + return this.client.writeCommand(command); + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/eb4f6059/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/client/ClientConnectionMultiplexor.java ---------------------------------------------------------------------- diff --git a/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/client/ClientConnectionMultiplexor.java b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/client/ClientConnectionMultiplexor.java new file mode 100644 index 0000000..9805a0b --- /dev/null +++ b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/client/ClientConnectionMultiplexor.java @@ -0,0 +1,43 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.iec60870.client; + +import org.apache.camel.component.iec60870.AbstractConnectionMultiplexor; + +public class ClientConnectionMultiplexor extends AbstractConnectionMultiplexor { + + private final ClientConnection connection; + + public ClientConnectionMultiplexor(final ClientConnection connection) { + this.connection = connection; + } + + @Override + protected void performStart() throws Exception { + this.connection.start(); + } + + @Override + protected void performStop() throws Exception { + this.connection.stop(); + } + + public ClientConnection getConnection() { + return this.connection; + } + +} http://git-wip-us.apache.org/repos/asf/camel/blob/eb4f6059/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/client/ClientConsumer.java ---------------------------------------------------------------------- diff --git a/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/client/ClientConsumer.java b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/client/ClientConsumer.java new file mode 100644 index 0000000..f947f29 --- /dev/null +++ b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/client/ClientConsumer.java @@ -0,0 +1,79 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.iec60870.client; + +import java.time.Instant; + +import org.apache.camel.Exchange; +import org.apache.camel.Message; +import org.apache.camel.Processor; +import org.apache.camel.component.iec60870.ObjectAddress; +import org.apache.camel.impl.DefaultConsumer; +import org.apache.camel.impl.DefaultMessage; +import org.eclipse.neoscada.protocol.iec60870.asdu.types.Value; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ClientConsumer extends DefaultConsumer { + + private static final Logger LOG = LoggerFactory.getLogger(ClientConsumer.class); + + private final ClientConnection connection; + private final ClientEndpoint endpoint; + + public ClientConsumer(final ClientEndpoint endpoint, final Processor processor, final ClientConnection connection) { + super(endpoint, processor); + this.endpoint = endpoint; + this.connection = connection; + } + + @Override + protected void doStart() throws Exception { + super.doStart(); + this.connection.setListener(this.endpoint.getAddress(), this::updateValue); + } + + @Override + protected void doStop() throws Exception { + this.connection.setListener(this.endpoint.getAddress(), null); + super.doStop(); + } + + private void updateValue(final ObjectAddress address, final Value<?> value) { + // Note: we hold the sync lock for the connection + try { + final Exchange exchange = getEndpoint().createExchange(); + exchange.setIn(mapMessage(value)); + getAsyncProcessor().process(exchange); + } catch (final Exception e) { + LOG.debug("Failed to process message", e); + } + } + + private Message mapMessage(final Value<?> value) { + final DefaultMessage message = new DefaultMessage(this.endpoint.getCamelContext()); + + message.setBody(value); + + message.setHeader("value", value.getValue()); + message.setHeader("timestamp", Instant.ofEpochMilli(value.getTimestamp())); + message.setHeader("quality", value.getQualityInformation()); + message.setHeader("overflow", value.isOverflow()); + + return message; + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/eb4f6059/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/client/ClientEndpoint.java ---------------------------------------------------------------------- diff --git a/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/client/ClientEndpoint.java b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/client/ClientEndpoint.java new file mode 100644 index 0000000..e554a4f --- /dev/null +++ b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/client/ClientEndpoint.java @@ -0,0 +1,46 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.iec60870.client; + +import static java.util.Objects.requireNonNull; + +import org.apache.camel.Consumer; +import org.apache.camel.Processor; +import org.apache.camel.Producer; +import org.apache.camel.component.iec60870.AbstractIecEndpoint; +import org.apache.camel.component.iec60870.ObjectAddress; +import org.apache.camel.impl.DefaultComponent; +import org.apache.camel.spi.UriEndpoint; + +@UriEndpoint(firstVersion = "2.20.0", scheme = "iec60870-client", syntax = "iec60870-client:endpointUri", title = "IEC 60870 Client", consumerClass = ClientConsumer.class, label = "iot") +public class ClientEndpoint extends AbstractIecEndpoint<ClientConnectionMultiplexor> { + + public ClientEndpoint(final String uri, final DefaultComponent component, final ClientConnectionMultiplexor connection, final ObjectAddress address) { + super(uri, component, requireNonNull(connection), address); + } + + @Override + public Producer createProducer() throws Exception { + return new ClientProducer(this, getConnection().getConnection()); + } + + @Override + public Consumer createConsumer(final Processor processor) throws Exception { + return new ClientConsumer(this, processor, getConnection().getConnection()); + } + +} http://git-wip-us.apache.org/repos/asf/camel/blob/eb4f6059/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/client/ClientOptions.java ---------------------------------------------------------------------- diff --git a/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/client/ClientOptions.java b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/client/ClientOptions.java new file mode 100644 index 0000000..86cdb9e --- /dev/null +++ b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/client/ClientOptions.java @@ -0,0 +1,91 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.iec60870.client; + +import java.util.Objects; + +import org.apache.camel.component.iec60870.BaseOptions; +import org.apache.camel.spi.UriParam; +import org.apache.camel.spi.UriParams; +import org.eclipse.neoscada.protocol.iec60870.ProtocolOptions; +import org.eclipse.neoscada.protocol.iec60870.client.data.DataModuleOptions; + +@UriParams +public class ClientOptions extends BaseOptions<ClientOptions> { + + /** + * Data module options + */ + @UriParam(javaType = "DataModuleOptions") + private DataModuleOptions.Builder dataModuleOptions; + + // dummy for doc generation + /** + * Whether background scan transmissions should be ignored. + */ + @UriParam(label = "data", defaultValue = "true") + private boolean ignoreBackgroundScan; + + public ClientOptions() { + this.dataModuleOptions = new DataModuleOptions.Builder(); + } + + public ClientOptions(final ClientOptions other) { + this(other.getProtocolOptions(), other.getDataModuleOptions()); + } + + public ClientOptions(final ProtocolOptions protocolOptions, final DataModuleOptions dataOptions) { + super(protocolOptions); + + Objects.requireNonNull(dataOptions); + + this.dataModuleOptions = new DataModuleOptions.Builder(dataOptions); + } + + public void setDataModuleOptions(final DataModuleOptions dataModuleOptions) { + Objects.requireNonNull(dataModuleOptions); + + this.dataModuleOptions = new DataModuleOptions.Builder(dataModuleOptions); + } + + public DataModuleOptions getDataModuleOptions() { + return this.dataModuleOptions.build(); + } + + @Override + public ClientOptions copy() { + return new ClientOptions(this); + } + + // wrapper methods - DataModuleOptions + + public void setCauseSourceAddress(final Byte causeSourceAddress) { + this.dataModuleOptions.setCauseSourceAddress(causeSourceAddress); + } + + public Byte getCauseSourceAddress() { + return this.dataModuleOptions.getCauseSourceAddress(); + } + + public void setIgnoreBackgroundScan(final boolean ignoreBackgroundScan) { + this.dataModuleOptions.setIgnoreBackgroundScan(ignoreBackgroundScan); + } + + public boolean isIgnoreBackgroundScan() { + return this.dataModuleOptions.isIgnoreBackgroundScan(); + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/eb4f6059/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/client/ClientProducer.java ---------------------------------------------------------------------- diff --git a/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/client/ClientProducer.java b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/client/ClientProducer.java new file mode 100644 index 0000000..de7c36d --- /dev/null +++ b/components/camel-iec60870/src/main/java/org/apache/camel/component/iec60870/client/ClientProducer.java @@ -0,0 +1,89 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.iec60870.client; + +import org.apache.camel.Exchange; +import org.apache.camel.component.iec60870.ObjectAddress; +import org.apache.camel.impl.DefaultProducer; +import org.eclipse.neoscada.protocol.iec60870.asdu.ASDUHeader; +import org.eclipse.neoscada.protocol.iec60870.asdu.message.SetPointCommandScaledValue; +import org.eclipse.neoscada.protocol.iec60870.asdu.message.SetPointCommandShortFloatingPoint; +import org.eclipse.neoscada.protocol.iec60870.asdu.message.SingleCommand; +import org.eclipse.neoscada.protocol.iec60870.asdu.types.CauseOfTransmission; +import org.eclipse.neoscada.protocol.iec60870.asdu.types.InformationObjectAddress; + +public class ClientProducer extends DefaultProducer { + + private final ClientConnection connection; + private final ASDUHeader header; + private final InformationObjectAddress address; + + public ClientProducer(final ClientEndpoint endpoint, final ClientConnection connection) { + super(endpoint); + this.connection = connection; + + final ObjectAddress address = endpoint.getAddress(); + this.header = new ASDUHeader(CauseOfTransmission.ACTIVATED, address.getASDUAddress()); + this.address = address.getInformationObjectAddress(); + } + + @Override + public void process(final Exchange exchange) throws Exception { + final Object command = mapToCommand(exchange); + + if (command != null) { + if (!this.connection.executeCommand(command)) { + throw new IllegalStateException("Failed to send command. Not connected."); + } + } + } + + private Object mapToCommand(final Exchange exchange) { + final Object body = exchange.getIn().getBody(); + + if (body instanceof Float || body instanceof Double) { + return makeFloatCommand(((Number)body).floatValue()); + } + + if (body instanceof Boolean) { + return makeBooleanCommand((Boolean)body); + } + + if (body instanceof Integer || body instanceof Short || body instanceof Byte || body instanceof Long) { + return makeIntCommand(((Number)body).longValue()); + } + + throw new IllegalArgumentException("Unable to map value to a command: " + body); + } + + private Object makeBooleanCommand(final Boolean state) { + return new SingleCommand(this.header, this.address, state); + } + + private Object makeIntCommand(final long value) { + + if (value < Short.MIN_VALUE || value > Short.MAX_VALUE) { + throw new IllegalArgumentException(String.format("Integer value is outside of range - min: %s, max: %s", Short.MIN_VALUE, Short.MAX_VALUE)); + } + + return new SetPointCommandScaledValue(this.header, this.address, (short)value); + } + + private Object makeFloatCommand(final float value) { + return new SetPointCommandShortFloatingPoint(this.header, this.address, value); + } +}